Pick a depth. Each prompt opens in your AI pre-loaded with the lesson. Click a row to preview the prompt.
useRef returns a plain object { current: T } that persists for the full lifetime of the component without triggering re-renders when mutated. DOM refs are the obvious use case, but the more important pattern is using a ref to store any value you need to read inside async callbacks or effects without creating a stale closure. The pattern is: store the latest value in a ref on every render, then read from the ref (not the state variable) inside the callback. This gives you always-current data without re-creating the callback.
A ref is a stable box React never clears. Assigning .current doesn't trigger a render. Reads from .current always see the latest assignment, making refs the escape hatch for stale closures in async code.
queryRef.current inside the setTimeout callback with the raw query variable (the state value from the closure). Type quickly and observe that some results show the wrong query. Switch back to queryRef.current and confirm the bug disappears.console.log('render', query) at the top of the component. Mutate queryRef.current = 'direct write' in a button's onClick. Confirm no extra render fires — useRef mutations are invisible to React's render cycle.queryRef.current = query (the assignment on every render) with useEffect(() => { queryRef.current = query }, [query]). This is a subtle bug: the effect runs asynchronously, so queryRef.current can be one render behind. Reproduce the stale result and explain why.inputRef.current.getBoundingClientRect() inside a regular event handler (not an effect). Confirm you get the latest layout — refs give you synchronous access to the DOM at any time, not just inside effects.import { useState, useRef, useEffect } from 'react';
// Pattern: always-current value inside a long-lived callback
export function SearchWithAbort() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<string[]>([]);
const queryRef = useRef(query); // mirror of query, no stale closure
// Keep the ref in sync with state on every render
queryRef.current = query;
useEffect(() => {
if (!query) return;
let cancelled = false;
const timer = setTimeout(async () => {
const data = await fakeSearch(queryRef.current); // read ref, not closure
if (!cancelled) setResults(data);
}, 300);
return () => {
cancelled = true;
clearTimeout(timer);
};
}, [query]);
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
<ul>{results.map(r => <li key={r}>{r}</li>)}</ul>
</div>
);
}
async function fakeSearch(q: string): Promise<string[]> {
return [`Result for "${q}" A`, `Result for "${q}" B`];
}
// DOM ref: focus an input imperatively
export function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} placeholder="auto-focused" />;
}