Passing an object in setState() without creating a new reference

Loading

A common React performance pitfall is passing objects to setState() (or state setters in functional components) without creating new references, which can lead to unexpected behavior and missed updates.

The Problem

// ❌ Wrong - mutating existing state object
const [state, setState] = useState({ count: 0 });

function increment() {
  state.count += 1;          // Direct mutation
  setState(state);           // Same object reference
}

Why this fails:

  1. React uses shallow comparison to detect state changes
  2. Same object reference appears as “no change”
  3. Component won’t re-render properly
  4. Can cause subtle bugs in your application

Correct Solutions

1. Spread Operator (Shallow Copy)

// ✅ Correct - creates new object reference
setState(prevState => ({
  ...prevState,             // Copy existing properties
  count: prevState.count + 1 // Update specific property
}));

2. Object.assign (Alternative to Spread)

// ✅ Correct - creates new object
setState(prevState => Object.assign({}, prevState, {
  count: prevState.count + 1
}));

3. Functional Updates (Recommended for Derived State)

// ✅ Correct - functional update ensures latest state
setState(prevState => {
  return {
    ...prevState,
    count: prevState.count + 1
  };
});

4. For Class Components

// ✅ Correct class component usage
this.setState(prevState => ({
  ...prevState,
  count: prevState.count + 1
}));

When You Can Reuse References

There’s one exception where you don’t need new references – when using state management libraries that handle immutability internally:

// With libraries like Immer, Redux, etc.
import produce from 'immer';

setState(produce(draft => {
  draft.count += 1; // Safe mutation - library handles copying
}));

Common Mistakes to Avoid

  1. Nested mutations without proper copying:
   // ❌ Wrong - nested mutation
   state.user.profile.name = "New Name";
   setState(state);
  1. Partial updates without copying:
   // ❌ Wrong - loses other state properties
   setState({ count: state.count + 1 });
  1. Array mutations:
   // ❌ Wrong - direct array mutation
   state.items.push(newItem);
   setState(state);

Performance Considerations

  1. Shallow copies are cheap for most state objects
  2. Deep cloning is expensive – avoid unless necessary
  3. Structural sharing (used by libraries like Immer) helps performance
  4. Memoization can help with derived state
// For deep objects, consider specialized libraries
import { produce } from 'immer';

function Component() {
  const [state, setState] = useState({
    user: {
      profile: {
        name: "John",
        preferences: {}
      }
    }
  });

  const updateName = () => {
    setState(produce(draft => {
      draft.user.profile.name = "Jane"; // Safe deep update
    }));
  };
}

Leave a Reply

Your email address will not be published. Required fields are marked *