Rendering Phases

March 20, 20255 min read

Every React update goes through two phases. Understanding what happens in each — and what's safe to do where — prevents entire categories of bugs.

Phase 1: Render (The "What")

The render phase is React's planning stage. React calls your component functions, walks the Fiber tree, and figures out what the UI should look like.

What happens:

  • Your component function executes
  • React calls useState, useReducer, useMemo, useCallback (reads current values, computes derived ones)
  • React compares the new output against the previous output (reconciliation)
  • React marks Fibers that need DOM updates

What does NOT happen:

  • The DOM is not touched
  • Side effects do not run
  • useEffect and useLayoutEffect callbacks do not fire

Key constraint: render must be pure.

Because the render phase is interruptible and can be restarted, your component function might be called multiple times. It must produce the same output for the same input — no side effects.

TSX
// ❌ Side effect in render — runs multiple times, unpredictable function Component() { fetch('/api/data'); // called on every render, possibly multiple times return <div />; } // ✅ Side effect in effect — runs once after commit function Component() { useEffect(() => { fetch('/api/data'); }, []); return <div />; }

Phase 2: Commit (The "Do")

After React knows what changed, it applies those changes. The commit phase is synchronous and uninterruptible — it must complete in one go.

The commit phase has three sub-phases:

Before Mutation

React reads the current DOM before changing it. getSnapshotBeforeUpdate (class components) runs here.

Mutation

React applies all DOM changes — inserting nodes, updating attributes, removing elements.

Layout

The DOM is updated. useLayoutEffect runs synchronously before the browser paints. This is where you can safely measure DOM elements and immediately update layout.

TSX
function Tooltip({ targetRef }) { const tooltipRef = useRef(); useLayoutEffect(() => { // DOM is updated, browser hasn't painted yet const targetRect = targetRef.current.getBoundingClientRect(); tooltipRef.current.style.top = `${targetRect.bottom}px`; // The browser will paint the tooltip in the right position // Users never see it in the wrong position }); return <div ref={tooltipRef} className="tooltip" />; }

After this, the browser paints, and then useEffect runs asynchronously.

The Full Sequence

Diagram

The useEffect Data-Fetching Problem

Knowing the sequence reveals a common anti-pattern. When you fetch data inside useEffect:

  1. React renders the component with no data (loading state)
  2. Commits to DOM — user sees a skeleton or spinner
  3. useEffect fires — API call goes out
  4. Response arrives, setState called
  5. React re-renders with data, commits again

That's two full render-commit cycles to show the user what they actually wanted to see. This was the only option for a long time, and there's nothing wrong with apps that do it — you didn't have a choice.

Suspense is the better alternative. When React hits a component wrapped in Suspense during the render phase, it fires the data request immediately and shows the fallback. If the data arrives before React finishes rendering the rest of the tree, you skip the flash entirely. And since you're dealing with the resolved value (not null | Data), TypeScript is happier too.

TSX
// Old: two renders, fighting null function Profile() { const [user, setUser] = useState(null); useEffect(() => { fetchUser().then(setUser); }, []); if (!user) return <Spinner />; return <div>{user.name}</div>; // TypeScript: user could be null } // New: one render, data guaranteed function Profile() { const user = use(fetchUser()); // suspends until resolved return <div>{user.name}</div>; // TypeScript: user is always User }

useEffect vs useLayoutEffect

useEffectuseLayoutEffect
When it firesAfter browser paintAfter DOM update, before paint
Blocks paint?NoYes
Use forData fetching, subscriptions, analyticsDOM measurements, scroll sync, tooltips
SSRSafe (runs on client only)Causes a warning on server

Default to useEffect. Only reach for useLayoutEffect when you truly need to measure the DOM before paint.

A note on useLayoutEffect: it runs synchronously inside the commit phase, which means it can block the entire commit if you do anything expensive in it. It's primarily for library authors building rendering tools. If you put an API call or any async work in there, you've re-introduced the blocking behaviour that Fiber was designed to eliminate. If you don't know that you need it, you don't need it.

Why Strict Mode Double-Invokes

In development, React's Strict Mode intentionally:

  1. Calls your component function twice during the render phase
  2. Calls useEffect cleanup and then the effect itself again

This surfaces bugs where:

  • Your render has side effects (they'll fire twice and you'll notice)
  • Your effect doesn't clean up properly (the cleanup will run before the effect re-fires)

In production, this double-invocation doesn't happen. Strict Mode is a developer tool for catching impurity, not a performance concern.

The Practical Rule

WhereWhat's safe
Component body (render)Reading state, computing derived values, returning JSX
useEffectData fetching, subscriptions, logging, timers
useLayoutEffectDOM measurements, scroll restoration, avoiding flicker
Event handlersEverything — they fire outside the render cycle

Practice what you just read.

useEffect vs useLayoutEffect
1 exercise