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.If the user types "react" quickly:
- 2.Fetch for "r" starts (500ms)
- 3.Fetch for "re" starts (500ms)
- 4.Fetch for "rea" starts (500ms)
- 5."r" results arrive -- displayed briefly
- 6."rea" results arrive -- displayed
- 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
useTransitionto mark updates as interruptible - Track request versions to discard stale responses
- Use React Query's built-in request deduplication and cancellation