React Performance12-03-202410 min read

Hooks Deep Dive: useEffect, useLayoutEffect, and useRef

A practical deep dive into the core React hooks that interact with the outside world and the DOM, with clear patterns, examples, and guidance.


Hooks Deep Dive: useEffect, useLayoutEffect, and useRef


React encourages rendering pure UI from props and state. However, real applications interact with the outside world. They read layout, work with browser APIs, subscribe to events, and invoke imperative logic. React provides three hooks for these scenarios: useEffect, useLayoutEffect, and useRef.


These tools are powerful but easy to misuse. This guide explains how they work, when to use each one, and how to avoid common pitfalls.




1. The Role of Effects


React's rendering phase must remain pure. You should never read or write to the DOM, perform side effects, or subscribe to external systems during rendering.


Effects run after React has committed the UI, which ensures that:


  • the DOM exists
  • updates do not interfere with rendering
  • subscriptions and cleanup are predictable

Effects are for interacting with systems outside React, not for transforming data that belongs inside render.




2. useEffect: Syncing with External Systems


useEffect runs after the browser paints. It is the default tool for most side effects.


Use it to:


  • attach and clean up event listeners
  • manage subscriptions
  • interact with browser APIs such as localStorage
  • perform non blocking measurements
  • handle asynchronous operations that cannot run on the server

Example: window resize listener


useEffect(() => {
  function handleResize() {
    console.log("resized");
  }
  window.addEventListener("resize", handleResize);

  return () => window.removeEventListener("resize", handleResize);
}, []);

Example: updating document title


useEffect(() => {
  document.title = `Active tab: ${tab}`;
}, [tab]);

What NOT to do in useEffect


Avoid using effects to derive values that could have been computed during render.


Bad:


useEffect(() => {
  setTotal(a + b);
}, [a, b]);

Better:


const total = a + b;

Effects should model interactions, not logic that belongs in state or props.




3. useLayoutEffect: Running Code Before Paint


useLayoutEffect fires after React commits the DOM but before the browser paints. It blocks visual updates until your effect finishes.


Use it only when you need to:


  • measure DOM layout
  • perform synchronous mutations
  • avoid visible flicker during UI adjustments

Example: measuring an element


const boxRef = useRef(null);

useLayoutEffect(() => {
  const rect = boxRef.current.getBoundingClientRect();
  console.log("Height:", rect.height);
}, []);

If you do not need synchronous DOM reads, prefer useEffect because it is non blocking.




4. useRef: Persistent and Mutable Values


Refs provide a stable container that persists across renders without causing re renders.


Use them for:


  • storing DOM elements
  • mutable values that should not trigger renders
  • storing instance variables
  • referencing previous values, timers, or external objects

Example: DOM access


const inputRef = useRef(null);

return <input ref={inputRef} />;

Example: storing mutable data


const previousValue = useRef(0);

if (value !== previousValue.current) {
  console.log("value changed");
  previousValue.current = value;
}

Refs do not trigger renders


Updating a ref does not re render the component. This is what makes refs suitable for imperatively storing values that React should ignore.




5. Combining the Three Hooks


A common pattern:


  • useRef stores an element
  • useLayoutEffect reads layout synchronously
  • useEffect reacts to the result asynchronously

Example:


const ref = useRef(null);
const [height, setHeight] = useState(0);

useLayoutEffect(() => {
  setHeight(ref.current.getBoundingClientRect().height);
}, []);

useEffect(() => {
  console.log("Height updated:", height);
}, [height]);



6. Common Pitfalls and How to Avoid Them


Pitfall 1: Using effects for logic that belongs in render


Effects should not compute derived values. Compute inside render instead.


Pitfall 2: Forgetting cleanup functions


Always clean up subscriptions, timers, and event listeners.


Pitfall 3: Overusing useLayoutEffect


It blocks painting. Only use it for layout measurement or synchronous DOM reads.


Pitfall 4: Misusing refs for computed values


Refs store stable and mutable values. Do not use them to manage reactive UI state.


Pitfall 5: Effects firing more often than expected


This usually happens when dependencies are unstable.


Bad:


useEffect(() => {
  doSomething();
}, [options]);

If options is recreated every render, the effect runs repeatedly.


Fix:


const stableOptions = useMemo(() => ({ dark }), [dark]);
useEffect(() => {
  doSomething();
}, [stableOptions]);



Final Thoughts


Effects and refs are essential for integrating React with the real world. The key is to understand when code should run and how React schedules updates. Most logic belongs inside render, not inside effects. When used intentionally, useEffect, useLayoutEffect, and useRef give you predictable and controlled interaction with the DOM and external systems.