[rule] prefer-set-state-callback for object type value
Problem Description
In React, when using useState to manage object-type values, developers often update the state without using the callback form of the setter function. This can lead to bugs when the update depends on the previous state, especially within asynchronous operations, effects, or event handlers.
For example, directly setting state like:
setUser({ ...user, age: 30 });
setItems([...items, newItem]);
can lead to stale state issues if multiple updates are queued or if user is outdated due to closures. React provides a callback form for setState to ensure updates are based on the latest state:
setUser(prev => ({ ...prev, age: 30 }));
setItems(prev => [...prev, newItem]);
// allow empty object
setUser({});
setItems([]);
However, this best practice is often forgotten or inconsistently applied, especially when dealing with objects.
Alternative Solutions
-
Documentation: Developers can be educated to always use the callback form. But this relies on discipline and code reviews.
-
TypeScript Linting: TypeScript can help to some extent, but it doesn't warn against non-callback usage of setState.
-
Custom ESLint Rule (Proposed): Enforce the callback usage for object-type state setters in useState, improving code reliability automatically.
Rule Name and Error Message
Rule Name: prefer-set-state-callback
Error Message: "Avoid setting object-type state directly. Use the callback form of setState to ensure you update based on the latest state."
Detail: This rule triggers when an object-type state (e.g., from useState({})) is updated using the direct form instead of the callback form.
Examples
❌ Incorrect
const [user, setUser] = useState({ name: 'John', age: 25 });
const [items, setItems] = useState(['a', 'b']);
function updateAge() {
setUser({ ...user, age: 30 }); // ❌ Triggers rule
const newUser = { ...user, }
setUser(newUser); // ❌ Triggers rule
}
function addItem(item: string) {
setItems([...items, item]); // ❌ Triggers rule
const newItems = [...items]
setItems([...items, item]); // ❌ Triggers rule
}
✅ Correct
const [user, setUser] = useState({ name: 'John', age: 25 });
const [items, setItems] = useState([{ id: 1, value: "a"}, { id: 2, value : "b" } ]);
function updateAge() {
setUser(prev => ({ ...prev, age: 30 })); // ✅ Correct usage
}
function resetUser() {
setUser({}); // ✅ Correct usage
setUser({ name:"",age: 0 }); // ✅ Correct usage
}
function addItem(item: string) {
setItems((items)=>[...items, {id:Date.now(), value: item }]); // ✅ Correct usage
}
function resetItem() {
setItems([]) // ✅ Correct usage
setItems([{ id:0 , value:"" }]) // ✅ Correct usage
}
Extra 🧪 for primitive value
const [count,setCount] = useState(0)
const [open,setOpen] = useState(false)
const toggle = () => setOpen(!open) // ❌ Triggers rule
const toggle = () => setOpen(open=>!open) ✅ Correct usage
const countActions = ()=> {
setCount(count +1) // ❌ Triggers rule
setCount(count - 1) // ❌ Triggers rule
setCount(count=>count + 1) ✅ Correct usage
setCount(count=>count - 1) ✅ Correct usage
}
Evaluation Checklist
- [x] I have had problems with the pattern I want to forbid
- [x] I could not find a way to solve the problem by changing the API of the problematic code or introducing a new API
- [x] I have thought very hard about what the corner cases could be and what kind of patterns this would forbid that are actually okay, and they are acceptable
- [x] I think the rule explains well enough how to solve the issue, to make sure beginners are not blocked by it
- [x] I have discussed this rule with team members, and they all find it valuable
I think that's a cool rule to have as I found myself not using the callback function inside a state setter. @Rel1cx I could do a PoC for that?
Some edge cases would be the usage of a Date object / class based objects maybe?
If the user does a date.setFullYear(2000) and then setDate(date)the rule would and should report a warning and would have the (maybe unintended) side effect of warning the user about some mutation (setFullYear mutates the date it's invoked on)
@Rel1cx I could do a PoC for that?
@possum-enjoyer Yes, please go ahead. FYI: This is where I am currently writing the heuristic detection for object type value packages/utilities/var/src/get-object-type.ts, Currently no-unstable-context-value and no-unstable-default-props are using it, which should save you some time.
@Rel1cx I could do a PoC for that?
@possum-enjoyer Yes, please go ahead. FYI: This is where I am currently writing the heuristic detection for object type value packages/utilities/var/src/get-object-type.ts, Currently no-unstable-context-value and no-unstable-default-props are using it, which should save you some time.
Yeah this will help me big time! Thanks a lot. I wanted to use the no-unstable-context rule as a reference for that anyway so thanks for saving me that time :D