Using state updates inside a loop without batching

A common React performance pitfall is making multiple state updates inside loops without proper batching, which can lead to:

  • Unnecessary re-renders
  • Performance bottlenecks
  • Inconsistent UI state
  • Race conditions

The Problem

// ❌ Wrong - multiple unbatched state updates in loop
function processItems(items) {
  items.forEach(item => {
    setItems(prev => [...prev, item]); // Triggers re-render each time
  });
}

Why this is problematic:

  1. Each setState triggers a re-render
  2. React may process updates separately (no batching)
  3. Can cause layout thrashing
  4. Leads to poor performance with large datasets

Correct Solutions

1. Single Batch Update (Recommended)

// ✅ Correct - single update with all changes
function processItems(items) {
  setItems(prev => [...prev, ...items]); // One update
}

2. UseReducer for Complex State Logic

// ✅ Better for complex operations
const [state, dispatch] = useReducer(reducer, initialState);

function processItems(items) {
  dispatch({ type: 'ADD_ITEMS', payload: items });
}

// Reducer handles batch update
function reducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEMS':
      return [...state, ...action.payload];
    // other cases
  }
}

3. Manual Batching in Class Components

// ✅ Class component batching
processItems(items) {
  this.setState(prev => ({
    items: [...prev.items, ...items]
  }));
}

4. Using ReactDOM.unstable_batchedUpdates (Legacy)

// ⚠️ Last resort for special cases
import { unstable_batchedUpdates } from 'react-dom';

unstable_batchedUpdates(() => {
  items.forEach(item => {
    setItems(prev => [...prev, item]);
  });
});

When Multiple Updates Are Necessary

If you truly need sequential updates (e.g., animations):

// ✅ Using functional updates to chain state changes
function sequentialUpdates() {
  setCount(prev => prev + 1);
  setFlag(prev => !prev); 
  // React 18+ batches these automatically
}

Common Anti-Patterns to Avoid

  1. AJAX calls in loops:
   items.forEach(async item => {
     const result = await fetchItem(item.id);
     setResults(prev => [...prev, result]); // ❌
   });
  1. Timers with state updates:
   for (let i = 0; i < 10; i++) {
     setTimeout(() => setCount(prev => prev + 1), i * 100); // ❌
   }
  1. Event listeners in loops:
   elements.forEach(el => {
     el.addEventListener('click', () => {
       setClicks(prev => prev + 1); // ❌
     });
   });

Performance Optimization Techniques

  1. Debounce/Rapid Updates:
   const [value, setValue] = useState(0);
   const debouncedSetValue = useMemo(() => debounce(setValue, 100), []);

   fastEvents.forEach(event => {
     debouncedSetValue(event.data); // Only final update commits
   });
  1. Virtualization for Large Lists:
   // Use react-window or react-virtualized
   <List height={600} itemCount={1000} itemSize={35}>
     {Row}
   </List>
  1. Web Workers for Heavy Processing:
   const worker = new Worker('processor.js');
   worker.onmessage = (e) => {
     setResults(e.data); // Single update
   };
   worker.postMessage(largeDataset);

React 18+ Automatic Batching

React 18 improves this with automatic batching:

// All these are batched together in React 18
function handleClick() {
  setName('John');
  setAge(30);
  setActive(true);
}

However, loops and asynchronous operations still benefit from manual batching approaches.

Leave a Reply

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