NeuroAgent

Why Splice Works But Filter Doesn't in React State

Discover why splice works but filter doesn't when updating React state. Learn about closure issues and how to fix them with proper React patterns.

Why does using splice work but filter doesn’t when updating React state based on another state variable?

I’m working with React and have a stateful list of stopwatches. When I try to remove an item from the list based on a selected index, I’ve noticed that two different approaches behave differently:

Working Approach

javascript
removeStopwatch={() => {
  setStopwatches(s => {
    const newArray = [...s];
    newArray.splice(selectedStopwatchIndex, 1);
    return newArray;
  });
}}

Non-Working Approach

javascript
removeStopwatch={() => {
  setStopwatches(s => {
    const newArray = [...s];
    return newArray.filter((_, i) => i !== selectedStopwatchIndex);
  });
}}

The first approach using splice works correctly and removes the selected stopwatch. However, the second approach using filter doesn’t work, even though they seem functionally equivalent.

I’ve verified that selectedStopwatchIndex has the correct value when the remove button is clicked. What am I missing about how React state updates work in this context?

Code Context

Here’s the relevant component structure:

javascript
function MainScreen() {
  const [stopwatches, setStopwatches] = useState([
    new Stopwatch('A'),
    new Stopwatch('B')
  ]);

  const [selectedStopwatchIndex, setSelectedStopwatchIndex] = useState(-1);

  let selectedStopwatch = selectedStopwatchIndex == -1
    ? null
    : stopwatches[selectedStopwatchIndex];

  return (
    <>
      <StopwatchList
        stopwatches={stopwatches}
        setStopwatches={setStopwatches}
        selectedStopwatchIndex={selectedStopwatchIndex}
        setSelectedStopwatchIndex={setSelectedStopwatchIndex}
      />
      <StopwatchDetails
        stopwatch={selectedStopwatch}
        removeStopwatch={() => {
          // The problematic function
        }}
      />
    </>
  );
}
javascript
function StopwatchDetails({ stopwatch, removeStopwatch }) {
  const [time, setTime] = useState('0:00:00');

  useEffect(() => {
    if (!stopwatch) return;

    const interval = setInterval(() => {
      setTime(stopwatch.getElapsedTimeFormatted());
    }, 100);

    return () => {
      clearInterval(interval);
    }
  }, [stopwatch]);

  return (
    <>
      <p>
        {stopwatch == null
          ? 'Info about the currently selected stopwatch will appear here!'
          : `Time: ${time}`}
      </p>
      <button onClick={removeStopwatch}>Remove</button>
    </>
  );
}

I’ve also created a CodeSandbox that demonstrates this issue: https://codesandbox.io/p/sandbox/recursing-napier-ly4td9

NeuroAgent

The filter approach doesn’t work due to a closure issue where the removeStopwatch function captures a stale value of selectedStopwatchIndex, while the splice approach appears to work because it relies on the current state value within the updater function.


Contents


Understanding the Closure Problem

When you pass the removeStopwatch function from MainScreen to StopwatchDetails, it captures the current value of selectedStopwatchIndex in its closure. Here’s what happens:

javascript
// In MainScreen component
<StopwatchDetails
  stopwatch={selectedStopwatch}
  removeStopwatch={() => {
    // This function captures selectedStopwatchIndex at creation time
    setStopwatches(s => {
      const newArray = [...s];
      newArray.splice(selectedStopwatchIndex, 1); // Uses stale value
      return newArray;
    });
  }}
/>

The problem is that selectedStopwatchIndex might change (become -1 or another value) between when the function is created and when it’s actually called by the button click in StopwatchDetails.


Why Splice Appears to Work

The splice approach seems to work because it uses the functional update form of setStopwatches:

javascript
setStopwatches(s => {
  // 's' is the current state value
  const newArray = [...s];
  newArray.splice(selectedStopwatchIndex, 1); // Still uses stale value
  return newArray;
});

However, this is misleading. The splice approach only appears to work because selectedStopwatchIndex happens to still be correct when the function executes, or because the error is less noticeable.


Why Filter Fails

The filter approach fails for the same reason - it uses the stale captured value:

javascript
removeStopwatch={() => {
  setStopwatches(s => {
    const newArray = [...s];
    return newArray.filter((_, i) => i !== selectedStopwatchIndex); // Stale value
  });
}}

The difference is more apparent with filter because:

  • Filter processes the entire array
  • If selectedStopwatchIndex is -1 (no selection), filter removes nothing
  • If selectedStopwatchIndex is outdated, it removes the wrong element

Solutions to Fix the Issue

Solution 1: Pass the Index Directly to the Handler

javascript
function MainScreen() {
  // ... existing state
  
  const handleRemoveStopwatch = (indexToRemove) => {
    setStopwatches(s => {
      return s.filter((_, i) => i !== indexToRemove);
    });
  };

  return (
    <>
      <StopwatchList
        stopwatches={stopwatches}
        setStopwatches={setStopwatches}
        selectedStopwatchIndex={selectedStopwatchIndex}
        setSelectedStopwatchIndex={setSelectedStopwatchIndex}
      />
      <StopwatchDetails
        stopwatch={selectedStopwatch}
        removeStopwatch={() => handleRemoveStopwatch(selectedStopwatchIndex)}
      />
    </>
  );
}

Solution 2: Use useCallback with Dependencies

javascript
function MainScreen() {
  // ... existing state
  
  const handleRemoveStopwatch = useCallback(() => {
    if (selectedStopwatchIndex === -1) return;
    setStopwatches(s => s.filter((_, i) => i !== selectedStopwatchIndex));
  }, [selectedStopwatchIndex]);

  return (
    <>
      <StopwatchList
        stopwatches={stopwatches}
        setStopwatches={setStopwatches}
        selectedStopwatchIndex={selectedStopwatchIndex}
        setSelectedStopwatchIndex={setSelectedStopwatchIndex}
      />
      <StopwatchDetails
        stopwatch={selectedStopwatch}
        removeStopwatch={handleRemoveStopwatch}
      />
    </>
  );
}

Solution 3: Include Index in StopwatchDetails

javascript
function StopwatchDetails({ stopwatch, index, removeStopwatch }) {
  // ... existing logic
  
  return (
    <>
      <p>
        {stopwatch == null
          ? 'Info about the currently selected stopwatch will appear here!'
          : `Time: ${time}`}
      </p>
      <button onClick={() => removeStopwatch(index)}>Remove</button>
    </>
  );
}

// In MainScreen:
<StopwatchDetails
  stopwatch={selectedStopwatch}
  index={selectedStopwatchIndex}
  removeStopwatch={(index) => setStopwatches(s => s.filter((_, i) => i !== index))}
/>

Best Practices for React State Updates

1. Avoid Closure Closures

Never pass functions that capture state values directly to child components. Instead:

  • Pass the value as a parameter
  • Use useCallback with proper dependencies
  • Create the handler in the child component

2. Prefer Immutability

Both splice and filter can be used immutably, but filter is generally preferred for its clarity:

javascript
// Good - immutable filter
setStopwatches(prev => prev.filter((_, i) => i !== index));

// Good - immutable spread + splice (more verbose)
setStopwatches(prev => {
  const newArray = [...prev];
  newArray.splice(index, 1);
  return newArray;
});

3. Consider Performance for Large Arrays

As noted in the research, for large arrays, both approaches have O(n) complexity, but filter processes the entire array while splice only processes the remaining elements after the removal point.

4. Use Immer for Complex Updates

For complex state updates, consider using Immer:

javascript
import { produce } from 'immer';

setStopwatches(prev => 
  produce(prev, draft => {
    draft.splice(selectedStopwatchIndex, 1);
  })
);

Conclusion

The core issue is a closure problem where the removeStopwatch function captures a stale value of selectedStopwatchIndex. Both splice and filter approaches would work correctly if they used the current state value, but the closure captures the value at function creation time.

Key takeaways:

  1. Always pass current state values as parameters to handlers
  2. Use useCallback with proper dependencies when passing handlers
  3. Filter is generally preferred for its clarity and immutability
  4. Be aware of closure issues when passing functions between components

The most reliable solution is to pass the index directly to the handler function or use useCallback with the correct dependencies to ensure you’re always working with the current state values.

Sources

  1. React Dev - Updating Arrays in State
  2. Stack Overflow - Using splice to update state in React
  3. Stack Overflow - What’s the advantage of using $splice over filter
  4. Ultimate Courses - Immutable Arrays in JavaScript
  5. GitHub - Immutability Helper
  6. Redux - Immutable Update Patterns