Let's say I have a React component that may receive multiple events that perform state changes after one interaction, as shown below:
const {useRef, useState} = React;
function App() {
const [_, setState] = useState(null);
const renderCount = useRef(0);
renderCount.current += 1;
console.log("rendering");
return (
<div>
{`Render count: ${renderCount.current} `}
<span onClick={() => {
console.log("span clicked");
setState({});
}}>
<button onClick={() => {
console.log("button clicked");
setState({});
}}>Send click events</button>
</span>
</div>
);
}
ReactDOM.createRoot(
document.getElementById("root")
).render( < App / > );
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
<div id="root"></div>
In the above example, after pressing the button to fire the click events, both event handlers run, but React batches the state updates to only rerender once.
However, if I attach these event handlers directly on the native HTML elements, as shown below, React rerenders twice per interaction, or rather after each event handler.
const {useRef, useState, useEffect} = React;
function App() {
const [_, setState] = useState(null);
const spanRef = useRef();
const btnRef = useRef();
const renderCount = useRef(0);
renderCount.current += 1;
console.log("rendering");
useEffect(() => {
const handler1 = () => {
console.log("span clicked");
setState({});
};
const handler2 = () => {
console.log("button clicked");
setState({});
};
const span = spanRef.current;
span.addEventListener("click", handler1);
const btn = btnRef.current;
btn.addEventListener("click", handler2);
return () => {
span.removeEventListener("click", handler1);
btn.removeEventListener("click", handler2);
};
}, []);
return (
<div>
{`Render count: ${renderCount.current} `}
<span ref={spanRef}>
<button ref={btnRef}>Send click events</button>
</span>
</div>
);
}
ReactDOM.createRoot(
document.getElementById("root")
).render( < App / > );
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
<div id="root"></div>
I suspect this has something to do with React's synthetic event system, but I'm trying to understand the behavior exactly. For instance, will React always immediately rerender state updates after native event listeners? If so, do effects still run in between rerenders, or are they deferred until all event handlers have been executed?
Either answers or references to official documentation are appreciated. And to clarify, this isn't for a particular project or problem I'm solving, I'm just trying to better understand React's behavior.
The difference is (very probably) that,
I'd expect this is not well documented by React, because you ask about the order in which things happen, which you are not supposed to care about in React.
I would even say that React might change the internal behavior at any time without changing the basic concept (e.g. the guarantees React makes about state).
Anyway, I'll try to make sense of it here (I'm sorry, I need to guess a bit) ...
Probably a relevant fact is that React attaches event handlers at the root.
You can confirm that by logging the .currentTarget
of the event.nativeEvent
property, e.g.:
<span onClick={ event => {
console.log("span clicked:", event.nativeEvent.currentTarget, event.nativeEvent.target);
setState({});
}}>
<button onClick={ event => {
console.log("button clicked:", event.nativeEvent.currentTarget, event.nativeEvent.target);
setState({});
}}>Send click events</button>
</span>
In javascript, nothing can run in parallel, so normal click events, initiated by e.g. the mouse, can not be executed while other javascript code is still running.
So the click events are queued, and each is executed only when the call stack (aka "execution stack") is empty.
Therefore,
setState
and the re-render of the virtual DOM,setState
and the re-render,I assume that React handles the events from multiple elements with the same internal event handler, and invokes the attached handlers using synthetic events.
As synthetic events are always executed synchronously, all attached event handlers are executed before the internal React event handler returns, i.e.
setState
, but then, instead of the re-render,setState
.It's not possible to inspect or "tap into" the already attached event handlers (apart from browser dev-tools like "inspect node" and getEventListeners), but you can further examine the order in which the events are executed if you
console.log
in a microtask
, which will illustrate that the call stack was empty.E.g.:
const handler1 = () => {
console.log("Native event: spanRef click");
queueMicrotask(() => { console.log('Native event: spanRef click (microtask)'); } )
setState({});
};
const handler2 = () => {
console.log("Native event: buttonRef click");
queueMicrotask( () => { console.log('Native event: buttonRef click (microtask)'); } )
setState({});
};
// ...
<span ref={ spanRef } onClick={ () => {
console.log('React event: span onClick');
queueMicrotask( () => {
console.log('React event: span onClick (microtask)');
})
setState({});
}}>
<button ref={ buttonRef } onClick={ () => {
console.log( 'React event: button onClick' );
queueMicrotask( () => {
console.log( 'React event: button onClick (microtask)' );
})
setState({});
}}>Send click events</button>
</span>
Which will log after a mouse click on the button:
rendering 1
Native event: buttonRef click \
rendering 2 | native button event completely executed
Native event: buttonRef click (microtask) /
Native event: spanRef click \
rendering 4 | native span event completely executed
Native event: spanRef click (microtask) /
(Here probably Reacts internal root event handler executed)
React event: button onClick \
React event: span onClick |
rendering 6 | All React events executed
React event: button onClick (microtask) |
React event: span onClick (microtask) /