Hooks let function components hold state and side effects. The subtle parts — which interviews target — are timing and identity.
useRef has two jobs
useRef returns a stable, mutable container ({ current }) that persists across renders and does not trigger a re-render when changed. That single behavior powers two distinct uses:
- Element refs — attach to a view (
<input ref={inputRef} />) to imperatively focus/measure/scroll. - Mutable instance values — store something that should survive renders but isn’t UI state: a timer id, the previous prop value, a “has mounted” flag, a websocket. Mutating
ref.currentis the escape hatch for “remember this without re-rendering.”
const renders = useRef(0);
renders.current++; // changes every render, never causes one
useEffect vs useLayoutEffect
Both run after render, but at different moments:
useEffectfires after paint (asynchronously). The screen updates first, then your effect runs. This is the default — data fetching, subscriptions, logging, timers.useLayoutEffectfires synchronously after the DOM/native tree is mutated but before paint. Use it only when you must read layout (measure a node) and mutate before the user sees a flicker. It blocks paint, so overusing it hurts performance — in RN, preferuseEffectunless you’re measuring.
The cleanup function you return runs before the effect re-runs and on unmount — essential for removing listeners and clearing timers to avoid leaks.
The rules of hooks (and why)
- Only call hooks at the top level — never inside conditions, loops, or nested functions.
- Only call them from React functions — components or other hooks.
The reason is concrete: React tracks hooks by call order, not by name. It keeps an ordered list of hook “slots” per component. If you call hooks conditionally, the order shifts between renders and React associates state with the wrong slot — corrupting your state. The linter enforces this for you.
Custom hooks
A custom hook is just a function starting with use that calls other hooks. It extracts stateful logic for reuse — not markup. Examples: useDebouncedValue, useOnlineStatus, useFetch, useInterval.
function useDebouncedValue(value, ms = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), ms);
return () => clearTimeout(id); // cleanup cancels the pending update
}, [value, ms]);
return debounced;
}
Two components using useDebouncedValue get independent state — custom hooks share logic, not state. That’s the key distinction from older patterns (HOCs, render props) which wrapped the tree; hooks compose flatly without “wrapper hell.”
useRef is a render-stable mutable box that doesn’t trigger re-renders — used both for element refs and for instance values like timers. useEffect runs after paint; useLayoutEffect runs synchronously before paint for layout measurement. Hooks must be called unconditionally at the top level because React tracks them by call order. Custom hooks are use-prefixed functions that extract stateful logic for reuse, each call getting its own independent state.”