React rendering/effect/cleanup order

I did some tests to get insight into how React orders its render, effect, and cleanup functions. I share the results in this post.

I used react version: 18.3.1. The main component is defined below.

let id = 0
function App({children}: {children: ReactNode}) {
	const myId = ++id
	console.log('render', myId)

	const [rerenderCheck] = useState(myId)
	console.log('rerenderCheck', rerenderCheck)

	const [, setState] = useState(0)
	useEffect(() => {
		console.log('effect', myId)
		let ended = false
		setTimeout(() => !ended && id <= 2 && setState(id), 3000)
		return () => {
			ended = true
			console.log('cleanup', myId)
		}
	})
	return children
}

Variables explanation:

  • myId: identify render.
  • rerenderCheck: check if the render is a rerender or a fresh render (after an amount, or, first render).

First version: no <StrictMode> wrapper, no state update

Component:

let id = 0
function App({children}: {children: ReactNode}) {
	const myId = ++id
	console.log('render', myId)

	const [rerenderCheck] = useState(myId)
	console.log('rerenderCheck', rerenderCheck)

	useEffect(() => {
		console.log('effect', myId)
		return () => console.log('cleanup', myId)
	})
	return children
}

Results:

render 1
rerenderCheck 1
effect 1

Comments: nothing special.


Second version: with <StrictMode> wrapper

Component: same as the previous, but with <StrictMode> wrapper

Result:

render 1
rerenderCheck 1
[grey] render 2
[grey] rerenderCheck 2
effect 2
cleanup 2
effect 2

Comments:

  • rerenderCheck 2: <StrictMode> unmounts and re-mount the component. It does NOT re-render.
  • effect 1, cleanup 1 do not appear: the first mount renders only, effect does not get run.
  • effect 2 runs twice, with a single cleanup 2 in the middle: in the second mount, <StrictMode> run effect →cleanup →effect of the second mount. It does NOT run the effect of the first mount.

Third version: no <StrictMode> wrapper, with state update

Component:

let id = 0
export default function App({children}: {children: ReactNode}) {
	const myId = ++id
	console.log('render', myId)

	const [rerenderCheck] = useState(myId)
	console.log('rerenderCheck', rerenderCheck)

	const [, setState] = useState(0)
	useEffect(() => {
		console.log('effect', myId)
		let ended = false
		setTimeout(() => !ended && id <= 2 && setState(id), 3000)
		return () => {
			ended = true
			console.log('cleanup', myId)
		}
	})
	return children
}

Results:

render 1
effect 1
[wait]
render 2
cleanup 1
effect 2
[wait]
rerender 3
cleanup 2
effect 3
--

Comments:

  • The order render 2cleanup 1: react re-renders (render 2) the component first, then clean the effect of the previous render (cleanup 1), then run the effect for the re-render ( effect 2).

Fourth version: with <StrictMode> wrapper, with state update

Component: same as the previous, but with <StrictMode> wrapper

Results:

render 1
[grey] render 2
effect 2
cleanup 2
effect 2
[wait]
render 3
[grey] render 4
cleanup 2
effect 4

Comments:

  • [grey] render 4: even for the re-render triggered by an event ( setTimeout), <StrictMode> re-renders the component twice, and only runs the effect of the second render ( effect 4).

Conclusion

<StrictMode>'s behavior at React 18.3.1 version:

For the first render of the global app

  • The app gets unmounted and re-mounted, NOT re-rendered.
  • The effect of the first mount NOT run.
  • The effect of the re-mount get run TWICE, with a cleanup called in between.

For the subsequent re-renders

  • The component gets re-rendered twice.
  • The effect of the first re-render NOT run.
  • The effect of the second rerender run ONCE.

React 19: with strict mode

Version: 19.0.0-beta-26f2496093-20240514

Component:

let id = 0
function App({children}: {children: ReactNode}) {
	const myId = ++id
	console.log('render', myId)

	const [rerenderCheck] = useState(myId)
	console.log('rerenderCheck', rerenderCheck)

	const [, setState] = useState(0)
	useEffect(() => {
		console.log('effect', myId)
		let ended = false
		setTimeout(() => !ended && id <= 2 && setState(id), 3000)
		return () => {
			ended = true
			console.log('cleanup', myId)
		}
	})
	return children
}

Results:

render 1
rerenderCheck 1
[grey] render 2
[grey] rerenderCheck 1
effect 2
[wait]
render 3
rerenderCheck 1
[grey] render 4
[grey] rerenderCheck 1
cleanup 2
effect 4

Comments:

  • rerenderCheck 1: always 1, never be 2, which means in react 19, <StrictMode> does not unmount the component in the first render, it re-uses the component and re-renders the component.
  • effect 2 shows only once before [wait]: in react 19, <StrictMode> does NOT run the effect twice anymore.
  • [grey] render 4 shows: in react 19, <StrictMode> repeat the re-render after a state update, same as of react 18 behavior.