Runnable lesson · press Try it on the examples to edit and run them here.
Users expect to search, filter and sort. In React this takes three steps: keep the raw data and the user’s choices in state, derive the visible list from them during rendering, then map it.
The recipe
const PRODUCTS = [
{ id: 1, name: "Keyboard", price: 49, inStock: true },
{ id: 2, name: "Mouse", price: 25, inStock: false },
{ id: 3, name: "Monitor", price: 189, inStock: true },
{ id: 4, name: "Cable", price: 9, inStock: false },
];
function App() {
const [onlyInStock, setOnlyInStock] = React.useState(false);
const [sortByPrice, setSortByPrice] = React.useState(false);
// Derive the visible list. This is not state: it is calculated from state on each render.
let visible = onlyInStock ? PRODUCTS.filter((p) => p.inStock) : PRODUCTS;
if (sortByPrice) {
visible = [...visible].sort((a, b) => a.price - b.price); // copy first!
}
return (
<div>
<button onClick={() => setOnlyInStock(!onlyInStock)}>Only in stock</button>{" "}
<button onClick={() => setSortByPrice(!sortByPrice)}>Sort by price</button>
<p>Showing {visible.length} of {PRODUCTS.length}</p>
<ul>
{visible.map((p) => (
<li key={p.id}>{p.name} - ${p.price}</li>
))}
</ul>
</div>
);
}
Notice what is stored in state: only the two booleans. The filtered and sorted list is derived. It cannot get out of sync with the choices, because it is recalculated every time. Storing visibleProducts in state as well would be a bug waiting to happen.
Sort a copy, never the original
Array.prototype.sort changes the array it is called on. If that array is state or a prop, you have just mutated it. Make a copy first:
[...items].sort(compare) // spread copy
items.slice().sort(compare) // slice copy
items.toSorted(compare) // built-in non-mutating version in modern browsers
toSorted, toReversed and toSpliced exist in current browsers; check feature support for the browsers you target.
Sorting strings and dates
function App() {
const names = ["Linus", "ada", "Grace"];
const [desc, setDesc] = React.useState(false);
const sorted = [...names].sort((a, b) => a.localeCompare(b, "en", { sensitivity: "base" }));
if (desc) sorted.reverse();
return (
<div>
<button onClick={() => setDesc(!desc)}>Z to A</button>
<ol>{sorted.map((n) => <li key={n}>{n}</li>)}</ol>
</div>
);
}
localeCompare sorts text correctly for people (case and accents), unlike the default sort, which compares character codes. For dates, compare the timestamps (new Date(a) - new Date(b)), or ISO date strings, which sort correctly as text.
Search and filter together
function App() {
const all = ["Orange", "Apple", "Mango", "Pear", "Grape"];
const [query, setQuery] = React.useState("");
const visible = all.filter((f) => f.toLowerCase().includes(query.trim().toLowerCase()));
return (
<div>
<input aria-label="Search" value={query} onChange={(e) => setQuery(e.target.value)} />
<p>Showing {visible.length}</p>
<ul>{visible.map((f) => <li key={f}>{f}</li>)}</ul>
</div>
);
}
Large lists
Filtering a few hundred items on every key press is instant. If you have thousands of items and typing feels slow, options are useMemo (see useMemo), useDeferredValue, paging (see Pagination Basics) or virtualising the list. Measure first; do not optimise a list that is already fast.
Key points
- Keep raw data and the user’s choices in state; derive the visible list while rendering.
- Always sort a copy, never state or props directly.
- Handle the “no matches” case; see Empty States.