The Problem

React 18's concurrent features allow rendering to be interrupted and resumed. This is great for responsiveness but introduces race conditions: an older update can complete after a newer one, showing stale data.

Symptoms

  • Search results show results for a previous query
  • Filtered data briefly shows wrong results before correcting
  • Form values flash between old and new states
  • Data appears to "rewind" before updating correctly

Real Error Scenario

```javascript function SearchPage() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]);

const handleSearch = async (q) => { setQuery(q); const data = await fetchResults(q); // Takes 500ms setResults(data); // If user types again, this might be stale! };

return ( <> <input onChange={e => handleSearch(e.target.value)} /> <Results data={results} /> </> ); } ```

  1. 1.If the user types "react" quickly:
  2. 2.Fetch for "r" starts (500ms)
  3. 3.Fetch for "re" starts (500ms)
  4. 4.Fetch for "rea" starts (500ms)
  5. 5."r" results arrive -- displayed briefly
  6. 6."rea" results arrive -- displayed
  7. 7."re" results arrive -- WRONG DATA shown!

How to Fix It

Fix 1: AbortController with Concurrent Queries

```javascript function SearchPage() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const controllerRef = useRef(null);

const handleSearch = async (q) => { setQuery(q); controllerRef.current?.abort(); // Cancel previous request controllerRef.current = new AbortController();

try { const data = await fetchResults(q, { signal: controllerRef.current.signal }); setResults(data); } catch (err) { if (err.name !== 'AbortError') console.error(err); } };

return ( <> <input onChange={e => handleSearch(e.target.value)} /> <Results data={results} /> </> ); } ```

Fix 2: useTransition for Non-Urgent Updates

```javascript import { useTransition } from 'react';

function SearchPage() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [isPending, startTransition] = useTransition();

const handleChange = (e) => { const value = e.target.value; setQuery(value); // Urgent: update input immediately

startTransition(() => { // Non-urgent: can be interrupted fetchResults(value).then(data => setResults(data)); }); };

return ( <> <input value={query} onChange={handleChange} /> {isPending && <div>Loading...</div>} <Results data={results} /> </> ); } ```

Fix 3: Version Tracking

```javascript function SearchPage() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const versionRef = useRef(0);

const handleSearch = async (q) => { setQuery(q); const currentVersion = ++versionRef.current; const data = await fetchResults(q);

// Only update if this is still the latest request if (currentVersion === versionRef.current) { setResults(data); } }; // ... } ```

Prevention

  • Always abort previous requests before starting new ones
  • Use useTransition to mark updates as interruptible
  • Track request versions to discard stale responses
  • Use React Query's built-in request deduplication and cancellation