Core concepts
hook
A function that lets a functional component "hook into" React features like state and lifecycle, without writing a class.
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
💡 All built-in hooks start with "use" — it's also a naming convention your own custom hooks must follow.
custom hook
A function you write yourself, starting with "use", that composes other hooks to share stateful logic between components.
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
return width;
}
💡 The main way to share logic (not markup) between components — the hooks-era replacement for higher-order components.
render
The process of React calling your component function to figure out what the UI should look like right now.
// Every call to setCount triggers a re-render of Counter
💡 A render doesn't always mean the DOM changes — React diffs the result and only touches the DOM where needed.
controlled component
A form element whose value is driven entirely by React state, not by the DOM itself.
<input value={name} onChange={e => setName(e.target.value)} />
💡 The opposite is an "uncontrolled component", which reads its value via a ref instead of state.
State
useState
Adds a piece of local state to a component. Returns the current value and a setter function.
const [isOpen, setIsOpen] = useState(false);
💡 Calling the setter schedules a re-render — it doesn't update the variable "in place" synchronously.
useReducer
An alternative to useState for more complex state logic — updates go through a reducer function instead of direct setters.
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: 'increment' });
💡 Reach for this when the next state depends on several related fields, or update logic gets hard to follow with useState alone.
lifting state up
Moving state from a child component to their shared parent, so multiple children can read and update the same value.
// Parent owns the state, passes value + setter down as props
<TabList active={active} onSelect={setActive} />
💡 The standard React pattern before reaching for context or a state library.
stale closure
A bug where a function captured an old value of state or props and keeps using it, even after the value has changed.
useEffect(() => {
const id = setInterval(() => console.log(count), 1000); // always logs the count from render #1
return () => clearInterval(id);
}, []); // missing "count" in the dependency array causes this
💡 The #1 cause of "why does my callback see an old value" bugs — almost always a missing dependency.
Effects & lifecycle
useEffect
Runs side effects (data fetching, subscriptions, manually changing the DOM) after React has committed the render to the screen.
useEffect(() => {
document.title = `${count} clicks`;
}, [count]);
💡 Runs after every render by default — the dependency array controls when it re-runs.
dependency array
The array passed as the second argument to useEffect/useMemo/useCallback, telling React when to re-run the effect or recompute the value.
useEffect(() => { fetchUser(id); }, [id]); // re-runs only when id changes
💡 An empty array [] means "run once on mount"; omitting it entirely means "run after every render".
cleanup function
The function an effect can return, which React runs before the effect re-runs and when the component unmounts.
useEffect(() => {
const sub = source.subscribe(handler);
return () => sub.unsubscribe(); // cleanup
}, [source]);
💡 Forgetting cleanup is the classic cause of memory leaks and "setState after unmount" warnings.
mount / unmount
Mount is when a component is first added to the DOM; unmount is when it's removed. Hooks let you approximate these with an empty dependency array.
useEffect(() => {
console.log('mounted');
return () => console.log('unmounted');
}, []);
💡 React doesn't guarantee exactly-once mount in Strict Mode during development — effects can run twice on purpose to surface bugs.
useLayoutEffect
Like useEffect, but fires synchronously after DOM mutations and before the browser paints — for measurements that must happen before the user sees a flicker.
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setHeight(height);
}, []);
💡 Blocks the browser paint until it finishes — use useEffect unless you specifically need to avoid visual flicker.
Performance
useMemo
Caches the result of an expensive calculation and only recomputes it when one of its dependencies changes.
const sorted = useMemo(() => items.slice().sort(compare), [items]);
💡 Not free — the memoisation itself has a cost. Only worth it for genuinely expensive computations.
useCallback
Returns a memoised version of a function that only changes if one of its dependencies changes — keeps the same function reference between renders.
const handleClick = useCallback(() => onSelect(id), [id, onSelect]);
💡 Mostly useful when passing callbacks to memoised child components (React.memo) that would otherwise re-render on every new function reference.
React.memo
A higher-order component that skips re-rendering a component if its props haven't changed (shallow comparison).
const Row = React.memo(function Row({ item }) {
return <li>{item.name}</li>;
});
💡 Only useful if the component actually re-renders often with the same props — profile before reaching for it.
unnecessary re-render
A component re-rendering even though nothing the user can see actually needs to change — a common source of sluggish UI.
// Every keystroke in an unrelated sibling re-renders this whole list
💡 Fixed with React.memo, useMemo/useCallback, or restructuring state so it lives closer to where it's used.
Refs
useRef
Creates a mutable value that persists across renders without causing a re-render when it changes — commonly used to reference a DOM node.
const inputRef = useRef(null);
useEffect(() => { inputRef.current.focus(); }, []);
💡 Unlike state, updating a ref's .current does not trigger a re-render — use it for values React itself doesn't need to react to.
forwardRef
Lets a component pass a ref it receives through to one of its own children, usually a DOM element.
const FancyInput = forwardRef((props, ref) => <input ref={ref} {...props} />);
💡 Needed because refs aren't regular props — they don't automatically pass through a custom component.
imperative handle
Customizes what a parent sees when it holds a ref to a child component, via useImperativeHandle, instead of exposing the raw DOM node.
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
}));
💡 An escape hatch for imperative APIs (focus, scroll, play) — most components should stay declarative and avoid this.
Rules & context
rules of hooks
Two constraints every hook call must follow: only call hooks at the top level (never in loops/conditions), and only call them from React functions.
// ❌ Wrong — conditional hook call
if (isLoggedIn) { useEffect(() => {...}); }
// ✅ Correct — condition inside the hook
useEffect(() => { if (isLoggedIn) {...} }, [isLoggedIn]);
💡 React relies on hooks being called in the same order every render — breaking this rule causes hard-to-debug state corruption.
useContext
Reads a value from the nearest matching Context.Provider above the component in the tree, without prop-drilling it through every level.
const theme = useContext(ThemeContext);
💡 Any component using useContext re-renders whenever that context value changes — split large contexts to avoid over-rendering.
prop drilling
Passing a value down through many layers of components that don't use it themselves, just to get it to a deeply nested child.
<A user={user}><B user={user}><C user={user}><D user={user} /></C></B></A>
💡 The problem useContext (or a state library) is designed to solve.