Last 30 Days
No notifications
Hooks let you use React features (state, lifecycle, context) inside functional components without writing classes. Beyond useState, React provides several essential hooks.
Runs after render. Handles data fetching, subscriptions, DOM mutations, and timers.
useEffect(() => {
// Effect runs after render
return () => { /* cleanup on unmount or re-run */ };
}, [dependencies]);| Dependency Array | Behavior |
[] | Runs once on mount |
[a, b] | Runs when a or b changes |
| Omitted | Runs after every render |
const inputRef = useRef(null);
inputRef.current.focus(); // Direct DOM access.current does not trigger re-render| Hook | Memoizes | Use Case |
useMemo | Computed value | Expensive calculations |
useCallback | Function reference | Stable callbacks for children |
const sorted = useMemo(() => items.sort(), [items]);
const handleClick = useCallback(() => doThing(id), [id]);Extract reusable logic into functions that start with use. They can call other hooks.
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => { /* listener */ }, []);
return width;
}Before hooks, only class components could hold state or run side effects. Hooks let plain function components do everything classes could β with simpler, more composable code.
A hook is just a function whose name starts with use (useState, useEffect, β¦) that taps into React's internals. You already met useState. This topic covers the rest of the daily toolkit.
1. Call hooks at the top level β never inside loops, conditions, or nested functions. React tracks them by *call order*; if the order changes between renders, your state mixes up. 2. Call hooks only from React functions β components or custom hooks. Not from regular utilities, not from event handlers.
A linter (eslint-plugin-react-hooks) enforces both. Trust it.
Render must be pure. Anything *side-effecty* β fetching data, subscribing to events, setting timers, touching the DOM β goes in useEffect.
import { useEffect, useState } from "react";function User({ id }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(/api/users/${id})
.then((r) => r.json())
.then(setUser);
}, [id]); // re-run when id changes
if (!user) return <p>Loadingβ¦</p>;
return <p>{user.name}</p>;
}
The second argument is the dependency array β a list of values the effect "depends on". React re-runs the effect whenever any of them change.
| Dep array | Effect runs |
| omitted | After every render (almost always wrong) |
[] | Once on mount only |
[a, b] | On mount + whenever a or b changes |
Return a function from your effect to clean up β React runs it before the next effect and on unmount.
useEffect(() => {
const id = setInterval(() => console.log("tick"), 1000);
return () => clearInterval(id); // cleanup on unmount or dep change
}, []);Forget cleanup and you leak intervals, listeners, subscriptions, and WebSockets.
useRef returns a mutable object { current: ... } that survives renders but does not trigger one when changed. Two main uses:
// 1. Reach into the DOM
const inputRef = useRef(null);
useEffect(() => inputRef.current.focus(), []);
return <input ref={inputRef} />;// 2. Hold a value across renders without re-rendering (timer ids, latest props)
const timerRef = useRef(null);
function start() {
timerRef.current = setInterval(tick, 1000);
}
function stop() {
clearInterval(timerRef.current);
}
Quick reference vs useState:
| Feature | useState | useRef |
| Re-renders on change? | Yes | No |
| Survives renders? | Yes | Yes |
| Use for | Anything visible | DOM refs, ids, mutable values |
When data needs to reach a deeply nested component (theme, current user, locale), Context lets you broadcast it.
import { createContext, useContext, useState } from "react";const ThemeContext = createContext("light");
function App() {
const [theme, setTheme] = useState("dark");
return (
<ThemeContext.Provider value={theme}>
<Page />
</ThemeContext.Provider>
);
}
function Button() {
const theme = useContext(ThemeContext); // anywhere in the tree
return <button className={theme}>Hi</button>;
}
Use Context for data that's truly global to a subtree. Don't use it as a poor-man's Redux for high-frequency updates β every consumer re-renders when the value changes.
1. Conditional hooks: if (x) useState(0) β forbidden. Always at the top level.
2. Missing dependencies in useEffect β leads to stale closures. The eslint plugin catches this.
3. Setting state inside an effect that runs every render with no deps β infinite loop.
4. Fetching in render β render must be pure. Fetch in useEffect (or in a framework's loader).
5. Forgetting cleanup β intervals and subscriptions pile up after each re-render.
These are memoisation hooks β they cache a value or function between renders.
const expensiveTotal = useMemo(() => {
return items.reduce((sum, it) => sum + it.price, 0);
}, [items]);const handleClick = useCallback(() => {
doSomething(id);
}, [id]);
When to use:
useMemo β when a computation is genuinely expensive (large array transforms).useCallback β when you pass a function down to a memoised child (React.memo) and want to keep its identity stable.Pair with useState when transitions are complex or several pieces of state move together (covered in State & Props). Often the cleanest option for forms with many fields.
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: "submit" });Any function that starts with use and calls hooks is a custom hook. Custom hooks let you share logic between components without sharing UI.
function useDebounce(value, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const t = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(t);
}, [value, delay]);
return debounced;
}function Search() {
const [q, setQ] = useState("");
const debouncedQ = useDebounce(q, 250);
useEffect(() => { fetchResults(debouncedQ); }, [debouncedQ]);
return <input value={q} onChange={(e) => setQ(e.target.value)} />;
}
This is the killer feature of hooks. Anything that used to need a HOC or render prop is now a custom hook.
Inside an effect, you "see" the values from the render that scheduled it. If you forget to list a dependency, you'll see *stale* data forever:
useEffect(() => {
const id = setInterval(() => console.log(count), 1000);
return () => clearInterval(id);
}, []); // β count is frozen at 0Two fixes:
1. Add count to deps so the interval restarts when it changes.
2. Or use the function form of state inside the effect (setCount(c => c + 1)).
Same API as useEffect but runs synchronously after DOM mutation, before paint. Use it when you must measure the DOM and immediately apply a style without flicker (tooltip positioning, scroll restoration). It blocks paint, so use sparingly.
React 18 ships concurrency hooks for keeping UI responsive during heavy renders.
const [isPending, startTransition] = useTransition();function handleSearch(value) {
setQuery(value); // urgent β keep input snappy
startTransition(() => {
setResults(slowFilter(value)); // non-urgent β can be interrupted
});
}
useDeferredValue(value) is the simpler cousin β gives you a "lagging" copy of a value that updates at lower priority.
Generate stable unique IDs for accessibility (associating labels with inputs in component libraries):
const id = useId();
return (
<>
<label htmlFor={id}>Email</label>
<input id={id} />
</>
);Works correctly with SSR (no hydration mismatch).
Lets a parent's ref expose a controlled API instead of the raw DOM node:
const FancyInput = forwardRef((props, ref) => {
const inner = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inner.current.focus(),
clear: () => { inner.current.value = ""; },
}));
return <input ref={inner} />;
});Niche, but invaluable for component libraries.
Subscribe to non-React stores (Redux, Zustand, browser APIs like window.matchMedia) in a way that's safe under concurrent rendering:
const isOnline = useSyncExternalStore(
(cb) => { window.addEventListener("online", cb); window.addEventListener("offline", cb); return () => { /* remove */ }; },
() => navigator.onLine,
() => true // server snapshot
);You'll mostly meet it inside library code, not your own.
1. Build a counter that auto-increments every second using useEffect + cleanup.
2. Build an useLocalStorage(key, initial) custom hook that reads/writes a value to localStorage.
3. Build a search box that uses your own useDebounce hook to avoid spamming an API.
4. Profile a slow list with React DevTools, then see whether useMemo actually helps. (Often it doesn't.)