How to Filter React State Arrays with Multiple Criteria: A Performance-First Approach

Use useMemo to derive filtered views from immutable state arrays while storing filter criteria in separate state variables to prevent unnecessary re-renders and preserve React's immutability contract.

When you need to filter React state arrays based on multiple criteria, the approach you choose directly impacts application performance and maintainability. The React source code in facebook/react demonstrates that state updates must be immutable—never mutating the array stored in useState—and that derived data should be computed efficiently to avoid redundant processing during render cycles.

Why Immutability Matters When You Filter React State Arrays

React's useState hook enforces immutability through its dispatcher implementation in [packages/react/src/ReactHooks.js](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js#L66-L70) and [packages/react/src/ReactClient.js](https://github.com/facebook/react/blob/main/packages/react/src/ReactClient.js#L52-L101). When you call the setter function returned by useState, React compares references to determine if an update is necessary.

If you mutate the existing array directly, React's comparison logic fails to detect changes, leading to stale UI states. Instead, you must create a new array reference whenever you update state:

// Wrong: Mutates existing array
items.push(newItem);
setItems(items);

// Correct: Creates new array reference
setItems(prev => [...prev, newItem]);

Optimizing Multi-Criteria Filters with useMemo

Filtering arrays runs in O(n) time complexity. Without optimization, React recalculates filtered results on every render, even when the source data and criteria haven't changed. The solution is useMemo, which caches the filtered array until its dependencies update.

Composing Predicate Functions

For multiple criteria, compose individual predicate functions into a single filter callback. This pattern improves readability and makes individual criteria reusable:

const byCategory = (item) => item.category === selectedCategory;
const byPriceRange = (item) => item.price >= minPrice && item.price <= maxPrice;
const byAvailability = (item) => item.inStock === true;

// Combined predicate
const filteredProducts = products.filter(
  item => byCategory(item) && byPriceRange(item) && byAvailability(item)
);

Complete Working Example

This implementation demonstrates filtering React state arrays with category and price criteria while maintaining optimal performance:

import React, { useState, useMemo, useCallback } from 'react';

const initialProducts = [
  { id: 1, name: 'Apple', category: 'Fruit', price: 1.2, inStock: true },
  { id: 2, name: 'Carrot', category: 'Veg', price: 0.8, inStock: false },
  { id: 3, name: 'Banana', category: 'Fruit', price: 1.1, inStock: true },
  { id: 4, name: 'Spinach', category: 'Veg', price: 2.5, inStock: true },
];

export default function ProductList() {
  // Source array remains immutable in state
  const [products] = useState(initialProducts);
  const [category, setCategory] = useState('Fruit');
  const [maxPrice, setMaxPrice] = useState(2);

  // Memoized predicate functions
  const byCategory = useCallback(
    (item) => item.category === category,
    [category]
  );
  
  const byPrice = useCallback(
    (item) => item.price <= maxPrice,
    [maxPrice]
  );

  // Derived filtered array - only recalculates when dependencies change
  const filtered = useMemo(() => {
    return products.filter((item) => byCategory(item) && byPrice(item));
  }, [products, byCategory, byPrice]);

  return (
    <div>
      <h2>Products</h2>
      
      <label>
        Category:
        <select 
          value={category} 
          onChange={(e) => setCategory(e.target.value)}
        >
          <option value="Fruit">Fruit</option>
          <option value="Veg">Vegetables</option>
        </select>
      </label>

      <label>
        Max price:
        <input
          type="number"
          value={maxPrice}
          onChange={(e) => setMaxPrice(Number(e.target.value))}
        />
      </label>

      <ul>
        {filtered.map((p) => (
          <li key={p.id}>
            {p.name} – ${p.price} ({p.category})
          </li>
        ))}
      </ul>
    </div>
  );
}

Handling Dynamic Criteria with Functional Updates

When users add or remove filter criteria dynamically, use functional updates to ensure you're working with the latest state. This pattern mirrors the useState dispatcher logic found in [ReactHooks.js](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js#L66-L70):

const [extraPredicates, setExtraPredicates] = useState([]);

const addInStockPredicate = () => {
  // Functional update ensures latest state
  setExtraPredicates((prev) => [
    ...prev,
    (item) => item.inStock === true,
  ]);
};

const filtered = useMemo(() => {
  return products.filter((item) => {
    // Base criteria
    if (!byCategory(item) || !byPrice(item)) return false;
    // Dynamic additional criteria
    return extraPredicates.every((pred) => pred(item));
  });
}, [products, byCategory, byPrice, extraPredicates]);

Key Source Files in the React Repository

Understanding the internal implementation helps explain why these patterns work:

File Significance
packages/react/src/ReactHooks.js Contains the useState implementation showing the functional update API (dispatchAction logic) that enables safe state transitions.
packages/react/src/ReactClient.js Exposes the public useState hook through the React client dispatcher, defining how state setters accept either values or updater functions.
packages/react/src/jsx/ReactJSXElement.js Demonstrates internal filtering logic used to separate special props like key and ref from regular props—showing React's own use of immutable filter patterns.

Summary

  • Never mutate state arrays directly—always create new references when updating data in useState.
  • Derive filtered views with useMemo to avoid O(n) recomputation on every render when filtering React state arrays.
  • Compose multiple criteria into individual predicate functions using useCallback, then combine them with logical operators inside the filter callback.
  • Use functional updates (setState(prev => ...)) when dynamically adding or removing filter criteria to ensure state consistency.
  • Keep source data and filter criteria separate in state to maintain clean data flow and prevent unnecessary re-renders.

Frequently Asked Questions

Should I store filtered results in useState or derive them with useMemo?

Derive them with useMemo. Storing filtered results in useState creates duplicate data sources and requires manual synchronization whenever the source array or criteria change. useMemo automatically recalculates only when dependencies change, keeping your data flow single-source-of-truth while optimizing performance.

How do I handle async filtering or large datasets?

For large datasets where O(n) filtering causes UI lag, move the filtering logic to a Web Worker or the server side. If you must filter client-side with async operations (like fetching filter metadata), use useEffect to handle the async logic and store the final filtered result in useState only after the computation completes, showing a loading state during processing.

Can I use useCallback instead of useMemo for filtering?

No, these serve different purposes. useCallback memoizes the filter function itself, while useMemo memoizes the result of the filtering operation. You should use useCallback for the individual predicate functions (as shown in the examples) to prevent them from being recreated on every render, but use useMemo to cache the final filtered array.

Why does React require immutability when updating arrays?

React uses reference equality checks (Object.is comparison) to determine if state has changed and whether to re-render. If you mutate an existing array, the reference stays the same, so React skips the update, leaving your UI stale. By creating new arrays, you guarantee that React detects the change and updates the component tree correctly, as implemented in the useState dispatcher logic in ReactHooks.js.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →