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

> Efficiently filter React state arrays by multiple criteria. Learn to use useMemo and immutable state for peak performance and a smooth user experience.

- Repository: [Meta/react](https://github.com/facebook/react)
- Tags: how-to-guide
- Published: 2026-02-19

---

**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)](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)](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:

```javascript
// 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:

```javascript
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:

```tsx
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/ReactHooks.js)](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js#L66-L70):

```tsx
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`](https://github.com/facebook/react/blob/main/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`](https://github.com/facebook/react/blob/main/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`](https://github.com/facebook/react/blob/main/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`](https://github.com/facebook/react/blob/main/ReactHooks.js).