Notifications

No notifications

/Phase 3

React Hooks

React Hooks β€” Side Effects, Refs & Performance

Hooks let you use React features (state, lifecycle, context) inside functional components without writing classes. Beyond useState, React provides several essential hooks.

useEffect β€” Side Effects

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 ArrayBehavior
[]Runs once on mount
[a, b]Runs when a or b changes
OmittedRuns after every render

useRef β€” Persistent Mutable Reference

const inputRef = useRef(null);
inputRef.current.focus(); // Direct DOM access

  • Persists between renders (unlike local variables)
  • Changing .current does not trigger re-render

useMemo & useCallback β€” Performance

HookMemoizesUse Case
useMemoComputed valueExpensive calculations
useCallbackFunction referenceStable callbacks for children

const sorted = useMemo(() => items.sort(), [items]);
const handleClick = useCallback(() => doThing(id), [id]);

Custom Hooks

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;
}

On this page

Detailed Theory

What Hooks Are (and Why)

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.

The Two Iron Rules

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.

useEffect β€” Side Effects in a Pure World

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 arrayEffect runs
omittedAfter every render (almost always wrong)
[]Once on mount only
[a, b]On mount + whenever a or b changes

Cleanup Functions

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 β€” A Box That Doesn't Trigger Re-Renders

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:

FeatureuseStateuseRef
Re-renders on change?YesNo
Survives renders?YesYes
Use forAnything visibleDOM refs, ids, mutable values

useContext β€” Skip the Prop Drilling

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.

Beginner Mistakes to Skip

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.

Intermediate: useMemo & useCallback

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.
When NOT to use:

  • For "performance just in case." Memoisation costs memory + comparison work; if the work is cheap, it's a net loss.
  • React may also drop the cache β€” it's a *hint*, not a guarantee.

Intermediate: useReducer

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" });

Intermediate: Custom Hooks β€” Reusable Logic

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.

Intermediate: useEffect Re-Runs and Stale Closures

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 0

Two 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)).

Advanced: useLayoutEffect

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.

Advanced: useTransition & useDeferredValue

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.

Advanced: useId

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).

Advanced: useImperativeHandle (with forwardRef)

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.

Advanced: useSyncExternalStore

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.

Practice Path

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.)