Keeping UI Responsive While Filtering Large Lists in React 18 with useTransition
Learn how React 18's automatic batching and the useTransition hook can keep your UI smooth when filtering thousands of items, with a concrete code example and trade‑offs to consider.
06 Jul 2026, 03:10 UTC

The problem: blocking UI when filtering a long list
When a user types into a filter box that controls a list of thousands of items, each keystroke triggers a state update that causes React to re‑render the entire list. If the list is large, the render work can block the browser’s main thread, leading to dropped frames and a laggy feel.
How React 18 helps: automatic batching and the useTransition hook
React 18 introduces concurrent rendering when you create a root with ReactDOM.createRoot. Two features that come with this mode are relevant:
- Automatic batching – multiple state updates that happen in the same event‑loop tick are grouped into a single render pass, reducing wasted work.
- useTransition hook – lets you mark a state update as non‑urgent. React can pause that update to let higher‑priority work (like handling user input) proceed, then finish the transition when the browser is idle.
Together, they give you a way to keep the UI responsive while still eventually showing the filtered list.
Worked example: filtering a 10 000‑item list
The following component shows a simple filter input and a list rendered from a large array. The filter state is updated inside a transition, so each keystroke is treated as low‑priority work.
import { useState, useTransition, useMemo } from 'react';
import { createRoot } from 'react-dom/client';
// Simulated large dataset
const allItems = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`);
function LargeListFilter() {
const [filter, setFilter] = useState('');
const [isPending, startTransition] = useTransition();
// Memoized filtered list – recomputed only when filter changes
const visibleItems = useMemo(() => {
if (!filter) return allItems;
const lower = filter.toLowerCase();
return allItems.item => item.toLowerCase().includes(lower);
}, [filter, allItems]);
return (
{
// The state update is wrapped in a transition
startTransition(() => setFilter(e.target.value));
}}
/>
{isPending && Filtering…
}
{visibleItems.map(item => (
- {item}
))}
);
}
// Render with concurrent mode enabled by default
const root = createRoot(document.getElementById('root'));
root.render();
What happens when the user types:
- The
onChangehandler callsstartTransition, which tells React to treat thesetFilterupdate as non‑urgent. - If the user types quickly, several
setFiltercalls may occur in the same tick; automatic batching groups them so React performs only one render for the batch. - While the transition is pending, the browser can still handle paint and input events, keeping the input feel smooth.
- When the browser is idle, React finishes the transition, recomputes the memoized filtered list, and updates the DOM.
Trade‑offs and limitations
- Perceived lag – because the filter update is deferred, there can be a short delay between typing and seeing the result. Mitigate this with a loading indicator (as shown) or optimistic UI.
- Stale data risk – if you accidentally mark an urgent update (e.g., form validation) as a transition, the user may see outdated information. Reserve
useTransitionfor truly non‑critical state like list filtering or background data loads. - Memory – memoizing the filtered list with
useMemoprevents recomputation on every render, but the memoized array still holds references to all items. For extremely large datasets consider virtualization or pagination.
Actionable steps to try it today
- Upgrade to React 18 (or ensure your project uses
ReactDOM.createRoot). - Identify a state change that is triggered by frequent user input and does not need to be synchronous (e.g., filtering, sorting, non‑critical background fetches).
- Wrap the state setter in
startTransitionfromuseTransition. - Verify the effect:
- Open React DevTools → Profiler, record a session while typing in the filter.
- Look for the number of commits: with batching/transition you should see fewer commits compared to a direct state update.
- Use the browser’s performance API or an FPS overlay to check that frame drops decrease.
- Add a loading indicator or optimistic UI if the delay becomes noticeable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.