React Rendering Order: Renders Go Down, Effects Come Up
By Sergey Nosov
16 October 2026
Put a console.log at the top of a React component and another inside its useEffect, then do the same in one of its children. The console prints the parent’s render before the child’s, but the child’s effect before the parent’s. In development, under Strict Mode, many of the lines appear twice. And when the two components share a Redux store, a click handled in the child can make the parent render first.
None of this is a bug. React handles an update in two phases. In the render phase, it calls your components, parents before children. In the commit phase, it applies the result to the DOM. Then it runs your effects, children before parents. Renders go down the tree; effects come up. Strict Mode, batching, and an external store such as Redux each add a twist, and React Compiler changes what the logs can tell you.
This article traces those orders through a small demo app, react-redux-render-order, which I published on GitHub. A parent renders two children. The components log their renders, memoized calculations, and effects, and Sibling’s reducer and three of the five click handlers log as well. I ran it in Chrome with React 19.3.1, the current release as of October 2026, and with React 19.0.0, the version its lock file pins. Both printed the same lines in the same order.
The Demo
The component tree is small:
App
└── Parent
├── Child
└── Sibling
Parent holds a number in useState and reads a counter from a Redux store with useSelector. Child keeps its own state and selects the same counter. Sibling has its own state too, a useReducer, the same counter, and a focusInput method that it exposes to Parent through useImperativeHandle. Each component logs a line when its function starts and another just before it returns, so every render shows up as a pair of Start and End lines. Child shows the pattern, with its JSX left out:
const Child = () => {
console.log('Child Render Start');
const [childState, setChildState] =
useState(0);
const dispatch: AppDispatch =
useDispatch();
const counter = useSelector(
(state: RootState) =>
state.counter.value);
const childRef =
useRef<HTMLDivElement>(null);
useEffect(() => {
console.log('Child useEffect');
}, []);
useLayoutEffect(() => {
console.log(
'Child useLayoutEffect');
}, []);
const memoizedChild = useMemo(() => {
console.log('Child useMemo');
return childState * 3;
}, [childState]);
const handleChildClick =
useCallback(() => {
console.log('Child useCallback');
setChildState(prev => prev + 1);
dispatch(decrement());
}, [dispatch]);
console.log('Child Render End');
// ...returns the JSX
};
Two labels in the log can mislead. The “Child useCallback” line is printed by the click handler that useCallback caches, so it appears when someone clicks, never during a render. React’s reference notes that, unlike useMemo, useCallback “does not call the function you provide.” Likewise, the “Sibling useReducer” line comes from inside Sibling’s reducer, so it appears whenever React calls the reducer.
To follow along, clone the repository, run npm install and npm run dev, open the address Vite prints, and open the browser console. Running npm run build and then npm run preview serves the production build.
First Load: Renders Go Down, Effects Come Up
Production builds leave out Strict Mode’s extra calls, so the order is easiest to read there. On page load, the production build prints the lines below, with the store’s state shortened.
App Render Start
App Render End
Parent Render Start
Parent useMemo
Parent Render End
Child Render Start
Child useMemo
Child Render End
Sibling Render Start
Sibling useMemo
Sibling Render End
Child useLayoutEffect
Sibling useLayoutEffect
Parent useLayoutEffect
Child useEffect
Sibling useEffect
Store state on mount: {…}
Parent useEffect
App useEffect
Render. The first eleven lines are the render phase. React’s guide defines rendering as “React calling your components.” React calls App, then Parent, then Child, then Sibling. Each function runs to its end before the next one starts, so Parent’s End line comes before Child’s Start line. Parent never calls Child. It returns JSX that names Child and Sibling, and React calls them afterward, in the order the JSX lists them. Parent’s useMemo line sits between its Start and End lines because React runs the calculation while it renders Parent.
A render line proves that React called the component, not that the result reached the screen. In a concurrent render, React’s version 18 announcement says, React “may even abandon an in-progress render altogether.” Effects run only after a commit, and never during server rendering.
Commit. Then React commits. It inserts the new DOM nodes first, then walks the tree again, children first, attaching refs and running layout effects as it goes: Child, Sibling, Parent. App has none. React’s reference says useLayoutEffect “fires before the browser repaints the screen,” with a guarantee that state updates scheduled inside it are processed before that repaint too. That makes a layout effect the place to measure a DOM node through a ref and adjust the layout before the user sees anything.
Measure the component’s own nodes there, though, not an ancestor’s. React’s guide to refs says React sets each ref right after updating the DOM, and it sets them in the same children-first walk that runs the layout effects. In a probe that passed Parent’s ref down to Child, Child’s layout effect found its own ref set but Parent’s still null on mount. By the time Parent’s layout effect ran, Parent’s ref was set.
Effects. The passive effects, the ones declared with useEffect, come last, and again children first: Child, Sibling, then Parent’s two effects in the order Parent declares them, then App. If an interaction did not cause the effect, React “will generally let the browser paint the updated screen first,” its reference says. After a click, React “may run your Effect before the browser paints the updated screen.” Neither is a promise about paint. Only the layout effect gets one.
Within each kind, then, a parent’s effect runs after its children’s. That is an order, not a promise that the children are ready. A parent’s layout effect still runs before its children’s passive effects, and any asynchronous work a child starts may still be pending.
The real trap is the reverse: a child that depends on its parent’s effect. Suppose App’s effect creates an analytics client, and a page deep in the tree records a page view from its own effect on mount. The page’s effect runs first and finds no client. React’s guide to Effects keeps such setup out of components altogether: “Some logic should only run once when the application starts. You can put it outside your components.”
Development: Strict Mode Calls the Pure Parts Twice
The demo’s main.tsx wraps the app in <React.StrictMode>. In development, Strict Mode calls some of your functions twice. React’s reference lists your component function body, though not the event handlers inside it, and “functions that you pass to useState, set functions, useMemo, or useReducer.” For App and Parent, the development server prints:
App Render Start
App Render End
App Render Start
App Render End
Parent Render Start
Parent useMemo
Parent useMemo
Parent Render End
Parent Render Start
Parent Render End
Each component renders twice in a row; the tree is not rendered twice from the top. Both “Parent useMemo” lines come from the first of Parent’s two renders. Strict Mode calls the calculation twice, and since React 19, the second render reuses “the memoized results from the first render,” as the React 19 upgrade guide puts it. React “will never call your event handlers twice,” says its reference, and each click in the demo prints its handler’s line once.
Strict Mode also gives every effect “one extra development-only setup+cleanup cycle before the first real setup.” The demo’s effects have no cleanup functions, so the plain log just shows each effect twice. With a logging cleanup added to each effect except the store-state one, the lines after the renders read:
Child useLayoutEffect
Sibling useLayoutEffect
Parent useLayoutEffect
Child useEffect
Sibling useEffect
Store state on mount: {…}
Parent useEffect
App useEffect
Parent useLayoutEffect cleanup
Child useLayoutEffect cleanup
Sibling useLayoutEffect cleanup
App useEffect cleanup
Parent useEffect cleanup
Child useEffect cleanup
Sibling useEffect cleanup
Child useLayoutEffect
Sibling useLayoutEffect
Parent useLayoutEffect
Child useEffect
Sibling useEffect
Store state on mount: {…}
Parent useEffect
App useEffect
The setups run children first, then the cleanups run parents first, layout before passive, and then the setups run again. React’s rule of thumb for passing this test is that a user cannot tell the single setup of production from the setup, cleanup, and second setup of development.
The extra cycle is not only a test. React 18 added it, its changelog says, to prepare for “a feature that lets components preserve state between unmounts.” React 19.2’s <Activity> works that way: in hidden mode, it “hides the children, unmounts effects,” while React keeps their state, and making them visible mounts the effects again. An effect that does not survive cleanup and setup breaks there too.
The doubled log lines themselves are harmless, because a log line changes nothing the program depends on. The entry on side-effect-free functions in my Software Development Principles series draws the same line. “A function that logs for debugging purposes has a technical side effect,” it says, “but may be treated as pure if the logging has no semantic significance.” A render that appends to a shared array, bumps a module-level counter, or sends a request is another matter. Strict Mode makes it happen twice, which is the point.
If React DevTools is installed, the log lines from the second render “will appear slightly dimmed,” and DevTools can hide them. Without the extension, nothing tells them apart: in the demo runs, the console received identical calls from both renders.
Updates: What One Click Renders
The production logs for clicks are short enough to read whole. Parent’s button sets Parent’s state and dispatches increment() to the store:
Parent useCallback
Parent Render Start
Parent useMemo
Parent Render End
Child Render Start
Child Render End
Sibling Render Start
Sibling Render End
The handler’s line comes first, because the handler runs before React renders anything. Then comes a single render pass, although the handler made two updates. React “waits until all code in the event handlers has run before processing your state updates,” and the store update joined the same pass. Two tools change that on purpose: flushSync forces React to render earlier, and a transition moves state updates, but not the store’s, into a separate, lower-priority render.
Child and Sibling rendered because their parent did: “React normally re-renders a component whenever its parent re-renders,” as the reference for memo says. Here they also select the counter, which changed. Child’s useMemo did not run, because childState did not change. None of the logged effects ran, because each has an empty dependency list or one that holds only the store, which never changes. Sibling’s useImperativeHandle has no dependency list, so it rebuilt its handle, but it logs nothing.
Sibling’s first button changes only Sibling’s state:
Sibling useCallback
Sibling Render Start
Sibling useMemo
Sibling Render End
Only Sibling rendered. A state update renders the component that owns the state and, unless React can skip them, its descendants. It never reaches up to a parent or across to a sibling.
Sibling’s second button dispatches an action to its reducer:
Sibling Render Start
Sibling useReducer
Sibling Render End
The reducer ran inside Sibling’s render, not when the button called dispatch. In development, the reducer line appears twice, both times inside the first of Sibling’s two renders, because Strict Mode calls reducers twice as well. Treat a reducer like a render: React calls it while rendering, so it must be pure.
The last button prints nothing. It calls Sibling’s focusInput, which reads a ref and focuses the input. No state changes, so nothing renders. Sibling still wraps itself in forwardRef, which works in React 19.3.1, but React 19 made the wrapper unnecessary: “In React 19, forwardRef is no longer necessary. Pass ref as a prop instead.” Here is Sibling in that style, with a dependency list so that the handle is not rebuilt on every render:
import { type Ref } from 'react';
const Sibling = ({ ref }: {
ref?: Ref<SiblingHandle>;
}) => {
// ...state and refs as before
useImperativeHandle(ref, () => ({
focusInput: () =>
inputRef.current?.focus(),
}), []);
// ...
};
That version passes the TypeScript compiler in strict mode, and the demo prints the same log with it.
Where Redux Fits
A Redux store lives outside React, so its updates arrive by another route. Redux’s documentation says dispatch calls the reducer synchronously and that “the change listeners will immediately be notified.” Redux Toolkit’s store delays that notice only for actions tagged for batching, which the demo does not use.
React Redux passes each notice on to every useSelector call. Its reference says the hook will “run your selector whenever an action is dispatched.” If the result differs from the previous one (by ===, unless you pass another comparison), “the component will be forced to re-render.” To watch that happen, I added a log line inside each component’s selector and another after each dispatch call. Child’s selector became:
const counter = useSelector(
(state: RootState) => {
console.log('Child selector');
return state.counter.value;
});
Then Child’s button, which sets Child’s state and dispatches decrement(), printed:
Child useCallback
Child selector
Sibling selector
Parent selector
Child handler: dispatch returned
Parent Render Start
Parent selector
Parent Render End
Child Render Start
Child selector
Child useMemo
Child Render End
Sibling Render Start
Sibling selector
Sibling Render End
All three selectors ran inside the dispatch call, before it returned and before any component rendered, and they ran children first: Child, Sibling, Parent. That is the order in which the components subscribed. React Redux subscribes through React’s useSyncExternalStore, which in React 19.3.1 subscribes from an effect, so on the first mount the children subscribed first.
React Redux then notifies subscribers in the order they subscribed. With hooks alone, a component that mounts later subscribes later, so the order follows mounting, not the tree. In a probe, a remounted Child ran its selector last. A store that notifies subscribers is the Observer pattern at work, with its usual question of who hears about a change first.
The render that followed went the other way: Parent, Child, Sibling, each running its selector again while it rendered. The click happened in Child, yet Parent rendered first, because Parent selects the counter too, and within a render pass React renders a parent before its children.
That window between the dispatch and the render is where React Redux’s documentation warns about “zombie children.” A child’s selector runs against the new state while the child still holds its old props. If the parent is about to stop rendering that child because its item was deleted, a selector that looks the item up by an ID from props can throw.
The documentation calls the problem “a rare concern” in practice. It says useSelector catches errors thrown during a store update and forces a render, which runs the selector again. That works as long as the selector is pure. Among the documentation’s remedies: “Don't rely on props in your selector function for extracting data,” or “try writing the selector functions defensively.”
React Redux adds a development check: on the first call, useSelector runs the selector “an extra time with the same parameter” and warns if the second result differs. The demo’s Parent also calls useStore, which returns the store but adds no subscription of its own. Parent’s effect logs the store’s state on mount, twice in development, and never again when the state changes. React Redux’s documentation warns that a component reading the store this way “will not automatically update if the store state changes.”
React Compiler Changes What the Logs Count
React Compiler reached its first stable release, version 1.0, on 7 October 2025. It is a build-time tool, and by default, the React team says, it “will memoize your code based on its analysis and heuristics.” I built the demo with it, using the Vite setup from React’s installation guide.
Renders and effects ran in the same order, but every render now printed its useMemo line. The compiled code shows why. The compiler had removed all three useMemo calls and computed each value inline, as a plain multiplication. The numbers on the screen stayed the same; only the log lines inside the calculations changed meaning.
That is within the rules. React’s reference says a useMemo calculation “should be pure” and that you “should only rely on useMemo as a performance optimization.” The compiler’s announcement adds that “changing memoization may under rare circumstances cause unexpected behavior,” and a log line inside a calculation is the simplest case of it. The demo’s lessons about order survive the compiler. Its counts do not.
The compiler can also skip a child. React’s reference for memo says the compiler “automatically applies the equivalent of memo to all components,” and adds that “memoization is a performance optimization, not a guarantee.” The compiler also leaves alone any component or hook it cannot optimize safely. In the demo, none of this showed: Child and Sibling select the counter themselves, and every click that renders Parent also changes it.
Seeing the Phases in Performance Tracks
Logs are a good way to learn the order and a poor way to count. React 19.2 added React Performance tracks to the browser’s Performance panel. A Scheduler track splits each render pass into Update, Render, Commit, and Remaining Effects, and a Components track shows which components rendered and for how long. The tracks’ documentation describes Remaining Effects as the useEffect reference does: “This usually happens after the paint,” with user interactions such as clicks as the known exception.
The tracks are “only available in development and profiling builds of React,” the documentation says; a profiling build uses react-dom/profiling in place of react-dom/client. For counts and timings, measure a production or profiling build. React’s reference for useMemo warns that measuring in development “will not give you the most accurate results.”
Rules to Keep
- Renders go down. React runs a parent’s function to its end, then its children’s, depth first, in the order the parent’s JSX lists them.
- Effects come up. Within one commit, layout effects run children first, then passive effects run children first. Inside one component, effects of each kind run in the order you declare them.
- Do not lean on a parent’s effect. Within each kind, a child’s effect runs before its parent’s, so put app-wide setup outside your components.
- Measure in a layout effect. Use
useLayoutEffectto measure a component’s own nodes before the user sees them, anduseEffectfor everything else, without assuming it runs after paint. - Expect two calls in development. Under Strict Mode, keep renders, calculations, and reducers pure, and make every cleanup undo its setup.
- Batching. Updates made synchronously in one event handler, Redux dispatches included, render in one pass, unless
flushSyncforces an earlier render or a transition defers some state updates. - Updates flow down. A state update renders its owner and, by default, the owner’s descendants. A store update schedules a render for every subscriber whose selection changed, and React renders them parents first.
- Count in production. Count renders in a production or profiling build, and expect React Compiler to change the counts but not the order.
The demo is small on purpose. Clone it, add a component, a cleanup, or a memo, and predict the console before you read it.
Further Reading
- react-redux-render-order (the demo app, GitHub)
- Render and Commit (React documentation)
- Synchronizing with Effects (React documentation)
- <StrictMode> (React documentation)
- useLayoutEffect (React documentation)
- Hooks (React Redux documentation)
- React Compiler v1.0 (React blog, 7 October 2025)
- React Performance Tracks (React documentation)