eslint-react icon indicating copy to clipboard operation
eslint-react copied to clipboard

[rule] prefer-set-state-callback for object type value

Open imteammy opened this issue 2 months ago • 3 comments

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

imteammy avatar Oct 19 '25 09:10 imteammy

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)

possum-enjoyer avatar Nov 15 '25 09:11 possum-enjoyer

@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 avatar Nov 15 '25 13:11 Rel1cx

@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

possum-enjoyer avatar Nov 15 '25 14:11 possum-enjoyer