Loading Now

React Hooks: useEffect, useCallback, useMemo, and useRef – When & How to Use Them

react-hooks

React Hooks: useEffect, useCallback, useMemo, and useRef – When & How to Use Them

React Hooks let function components manage state and side effects while keeping code concise and composable. This article focuses on when to use each hook and how to structure them for readability and performance.

useEffect: Side Effects (after render) with Optional Cleanup

Use ‘useEffect’ whenever you have side effects – logic that must run after React has painted changes to the DOM or after state/props change. Typical effects include fetching, subscribing, logging, updating document title, and imperative DOM interaction.

  • Runs after the component has rendered (painted to the screen).
  • If you return a function (cleanup), React calls it before the effect runs again on subsequent updates and during unmount.
useEffect(() => {
  // effect: runs after render when dependencies change

  return () => {
    // cleanup: runs before next effect and on unmount
  };
}, [dependencies]);

Patterns

1) Effect reacting to a particular state change

useEffect(() => {
  // execute when 'state' changed
  // e.g., sync to localStorage, fire analytics, refetch with new params
  console.log('state changed:', state);

  return () => {
    // execute before 'state' changes again (the next effect) and on unmount
    console.log('cleanup before next state change/unmount');
  };
}, [state]);

2) Effect on mount/unmount only (no dependencies)

useEffect(() => {
  // execute when component mounts (once)
  console.log('mounted');

  return () => {
    // execute when component will unmount (once)
    console.log('unmounted');
  };
}, [])

‘useEffect’ does not run before the first render – it runs after the render phase. If you need synchronous DOM measurements right after DOM mutations, consider ‘useLayoutEffect’ (used sparingly to avoid blocking paint).


useCallback: Memoize Functions Based on Dependencies

Use ‘useCallback’ when you define a function inside a component that:

  • Is passed down to children (to prevent unnecessary re-renders when children are ‘React.memo’-ized).
  • Is used in effects or as an event handler and you want a stable reference unless specific dependencies change.

Without ‘useCallback’, the function is recreated on every render, which can cause wasted renders in memoized children or unstable effect dependencies.

const myFunction = useCallback(() => {
  // your logic
}, [dependencies]);

Example:

function SearchBox({ onSearch }) {
  const [query, setQuery] = useState('');

  const handleChange = useCallback((e) => {
    setQuery(e.target.value);
  }, []); // does not depend on external state

  useEffect(() => {
    onSearch(query);
  }, [query, onSearch]);

  return <input value={query} onChange={handleChange} />;
}

useMemo: Memoize Derived Values Based on Dependencies

Use ‘useMemo’ when you compute a derived value (e.g., filtered list, expensive calculation) that should only recompute when its dependencies change. This reduces unnecessary recomputation and keeps references stable.

const myValue = useMemo(() => {
  // return calculated value
}, [dependencies]);

Example:

const sorted = useMemo(() => {
  return [...items].sort((a, b) => a.localeCompare(b));
}, [items]);

// Without useMemo, 'sorted' is recomputed and new-array referenced on every render.

Rule of thumb: only memoize calculations that are noticeably expensive or where stable identity is required for downstream memoization.


“ComponentWillMount” Trick – A Word of Caution

useMemo(() => { 
  // execute componentWillMount logic 
}, []);

Clarification:

  • ‘useMemo’ is meant to compute and return a value during render, not to perform side effects.
  • Side effects (like logging, subscriptions, network requests) must not run during render. Doing so can cause bugs in strict mode and concurrent rendering.
  • If you need to run something once after mount, use ‘useEffect(() => { … }, [])’.

Safe alternative if you truly need a computed value once:

const initialExpensiveValue = useMemo(() => {
  // compute a value (pure, no side effects)
  return computeInitialValue();
}, []);

If you think you need true “before first paint” side effects, revisit the design. In rare cases where synchronous measurement after paint is required, use ‘useLayoutEffect’.


Using Memoized Functions/Values as Dependencies

Functions from ‘useCallback’ and values from ‘useMemo’ can be used as dependencies in ‘useEffect’, ‘useCallback’, and ‘useMemo’. This is good practice because it produces a clear, readable dependency graph and avoids stale closures.

const computeParams = useMemo(() => ({ limit, sort }), [limit, sort]);

const fetchData = useCallback(async () => {
  const res = await api.list(computeParams);
  setData(res);
}, [computeParams]);

useEffect(() => {
  fetchData();
}, [fetchData]);
  • ‘computeParams’ is stable unless ‘limit’/’sort’ change.
  • ‘fetchData’ is stable unless ‘computeParams’ changes.
  • The effect cleanly depends on ‘fetchData’.

useRef: Mutable Value That Persists Without Re-render

Use ‘useRef’ to store a value that persists across renders without causing a re-render when updated. Typical use cases:

  • Storing DOM nodes (‘ref’ props).
  • Keeping mutable instances or caches.
  • Tracking previous values, timers, and flags.
function Timer() {
  const intervalIdRef = useRef(null);
  const [ticks, setTicks] = useState(0);

  useEffect(() => {
    intervalIdRef.current = setInterval(() => setTicks(t => t + 1), 1000);
    return () => clearInterval(intervalIdRef.current);
  }, []);

  return <div>Ticks: {ticks}</div>;
}
function Price({ value }) {
  const prev = useRef(value);
  useEffect(() => {
    prev.current = value;
  }, [value]);

  return <p>Current: {value} (Prev: {prev.current})</p>;
}

Best Practices & Pitfalls

  • Keep effects pure: inside ‘useEffect’, run side effects and return cleanups—don’t mutate during render.
  • Use dependency arrays honestly: list all values used in the effect/callback/memo. If this creates loops, refactor to memoize inputs or move logic.
  • Memoization is not a silver bullet: only use ‘useMemo’/’useCallback’ when identity stability or expensive work warrants it; otherwise you add complexity for little gain.
  • Cleanup correctly: unsubscribe, clear timers, cancel requests in the cleanup function to avoid memory leaks.
  • Don’t run side effects in render (‘useMemo’ should be pure and return a value).
  • Beware stale closures: missing dependencies lead to logic reading old state. Prefer adding dependencies and memoizing upstream values/functions.
  • ‘useLayoutEffect’ only when needed: it blocks the browser from painting; use sparingly for layout measurements.

Quick Reference

  • useEffect(fn, deps) – Run ‘fn’ after render when ‘deps’ change; return a cleanup to run before the next effect/unmount. ‘deps=[]’ → mount/unmount only.
  • useCallback(fn, deps) – Memoize a function; stable reference unless ‘deps’ change.
  • useMemo(calc, deps) – Memoize a computed value; re-computes only when ‘deps’ change.
  • useRef(initial) – Persistent mutable container ‘{ current }’; updating it does not re-render.

Post Comment