Skip to main content

Remove derived React state from a filtered list

2026-11-11 16:36:28.027738+00:00
Remove derived React state from a filtered list

A filtered inbox needs state for the user's filter and for messages received from outside the component. The visible subset can be calculated from those inputs whenever React renders. Keeping another state variable for that subset creates an extra value that can fall behind.

Consider synthetic messages with an unread flag. An implementation that starts with an empty visible array and fills it in an Effect first renders the empty array. The Effect then schedules another render. Every new filter condition must also appear in the Effect's dependencies. React's guidance on unnecessary Effects explains why calculations used for rendering belong in render.

function Inbox({ messages }) {
  const [unreadOnly, setUnreadOnly] = useState(false);
  const visible = messages.filter(
    message => !unreadOnly || message.unread
  );

  return (
    <>
      <label>
        <input type="checkbox" checked={unreadOnly}
          onChange={event => setUnreadOnly(event.target.checked)} />
        Unread only
      </label>
      <ul>{visible.map(message =>
        <li key={message.id}>{message.subject}</li>
      )}</ul>
    </>
  );
}

This component excerpt assumes useState is imported from React. Its filtering logic is deliberately small: an unchecked box includes every message, and a checked box includes only unread messages. The original messages remain available for counts, detail views or another filter.

Filter and message inputs feeding one derived list.

Check the state you removed

Use two messages, one read and one unread. Toggle the checkbox, then supply a replacement messages array containing a newly unread message. The displayed list should follow both changes without waiting for a synchronization Effect. Also try an empty array; an empty list is a valid result and should have an understandable empty-state message in a finished interface.

Avoid mutating messages during calculation. For example, sorting that array directly would change a value owned by the caller. Make a copy before sorting. Keeping render calculations pure also makes repeated development renders easier to reason about.

A large expensive filter can justify memoization after measurement. Memoization still derives the list from its inputs; it does not require a separate copy of the result in state. Any memoized calculation must include every input that affects it.

Editable drafts are a different case. A user who changes a message locally may intentionally diverge from the server's current value. Give that draft an explicit lifetime and save policy. Removing a redundant visible-list variable does not decide how remote data is fetched, how loading errors appear, or whether an unsaved edit should survive navigation.