The React 18 Brain Teaser That Stumps Seniors
If you have been writing React for more than a few years, your brain is likely hardwired to expect certain behaviors from the rendering engine. But React 18 introduced a massive fundamental change under the hood that quietly broke how many of us model state updates in our heads.
Let’s test your knowledge of modern React rendering mechanics. Assuming this app is running in standard production mode (no StrictMode double-invocations), take a look at the component below.
The Challenge
When the component mounts, and the user clicks the "Trigger" button exactly once, what will be logged to the console from start to finish?
import { useState, useRef } from 'react';
export default function App() {
const [count, setCount] = useState(0);
const renders = useRef(0);
renders.current++;
console.log(`Render ${renders.current}: ${count}`);
const handleClick = () => {
// First batch
setCount(c => c + 1);
setCount(c => c + 1);
// Second batch
setTimeout(() => {
setCount(c => c + 1);
setCount(c => c + 1);
}, 0);
};
return <button onClick={handleClick}>Trigger</button>;
}
Trace the state updates and the Event Loop in your head. Do you have your answer? Drop your guess in the comments below before you expand the solution!
Click here to reveal the output and explanation
The Correct Output:
Render 1: 0
Render 2: 2
Render 3: 4
Why does this happen?
If you guessed Render 3: 3 and Render 4: 4 at the end, you are thinking in React 17!
Here is exactly how the React 18 rendering engine processes this:
- Render 1 (Mount): The component mounts,
renders.currentbecomes 1, and count is 0. - The Click (Synchronous): React has always batched state updates inside standard React event handlers. The first two
setCountcalls are bundled together. React waits for the synchronous code to finish, applies both increments, and triggers exactly one render. (Logs Render 2: 2). - The Timeout (Asynchronous): In React 17, state updates inside promises, timeouts, or native event handlers were not batched. They would trigger a re-render for every single
setStatecall. - React 18 Automatic Batching: React 18 introduced Automatic Batching. Now, React batches state updates no matter where they happen. When the macro-task (the
setTimeout) executes, React intelligently batches the two internalsetCountcalls together just like it did in the synchronous event handler. It waits for the callback to complete, applies both increments, and triggers just one final render. (Logs Render 3: 4).
Bonus tip: If you ever actually need the old React 17 behavior to force the DOM to paint between updates inside a timeout, you now have to explicitly wrap your state updates in flushSync() from react-dom.