I understand the difference between useEffect
and useLayoutEffect
but I am curious how it is implemented.
I created this Sandbox which shows the order that things occur:
Log.js
:
export default function Log(count, ref) {
console.log(`Synchronous ${count}: Ran after ${ref.current}`);
queueMicrotask(() =>
console.log(`Microtask ${count}: Ran after ${ref.current}`)
);
setImmediate(() => console.log(`Task ${count}: Ran after ${ref.current}`));
}
App.scala
:
import { forwardRef, useEffect, useLayoutEffect, useState } from "react";
import Log from "./Log";
const App = forwardRef((props, ref) => {
const [, setCount] = useState(0);
ref.current = "render";
Log(2, ref);
useLayoutEffect(() => {
ref.current = "useLayoutEffect";
});
Log(3, ref);
useEffect(() => {
ref.current = "useEffect";
});
Log(4, ref);
return (
<button
onClick={() => {
console.log("Click");
setCount((x) => x + 1);
}}
>
Click
</button>
);
});
export default App;
index.js
:
import React, { StrictMode } from "react";
import ReactDOM from "react-dom";
import App from "./App";
import Log from "./Log";
const rootElement = document.getElementById("root");
const ref = React.createRef();
ref.current = "start";
Log(1, ref);
ReactDOM.render(
<StrictMode>
<App ref={ref} />
</StrictMode>,
rootElement
);
Log(5, ref);
After running the App and clicking the button the console shows:
Synchronous 1: Ran after start
Synchronous 2: Ran after render
Synchronous 3: Ran after render
Synchronous 4: Ran after render
Synchronous 5: Ran after useLayoutEffect
Microtask 1: Ran after useLayoutEffect
Microtask 2: Ran after useLayoutEffect
Microtask 3: Ran after useLayoutEffect
Microtask 4: Ran after useLayoutEffect
Microtask 2: Ran after useLayoutEffect
Microtask 3: Ran after useLayoutEffect
Microtask 4: Ran after useLayoutEffect
Microtask 5: Ran after useLayoutEffect
Task 1: Ran after useLayoutEffect
Task 2: Ran after useLayoutEffect
Task 3: Ran after useLayoutEffect
Task 4: Ran after useLayoutEffect
Task 2: Ran after useLayoutEffect
Task 3: Ran after useLayoutEffect
Task 4: Ran after useLayoutEffect
Task 5: Ran after useEffect
Click
Synchronous 2: Ran after render
Synchronous 3: Ran after render
Synchronous 4: Ran after render
Microtask 2: Ran after useLayoutEffect
Microtask 3: Ran after useLayoutEffect
Microtask 4: Ran after useLayoutEffect
Microtask 2: Ran after useLayoutEffect
Microtask 3: Ran after useLayoutEffect
Microtask 4: Ran after useLayoutEffect
Task 2: Ran after useEffect
Task 3: Ran after useEffect
Task 4: Ran after useEffect
Task 2: Ran after useEffect
Task 3: Ran after useEffect
Task 4: Ran after useEffect
The first thing that I noticed was:
Synchronous 5: Ran after useLayoutEffect
This shows that useLayoutEffect
is firing before microtasks that have been scheduled before it. This means that it is not implemented as a microtask but must be some internal task queue (not really that surprising).
The next thing was:
Task 4: Ran after useLayoutEffect
Since useEffect
yields to the browser to render and run other tasks I was expecting it to be implemented as a task and so be scheduled between task 3 and 4 in the next event loop iteration.
The only explanation that I can come up with as to why it occurs between task 4 and 5 is that React is creating a task but instead of scheduling it when the hook is called it queues it internally and then after render has completed it creates a task to execute that effect queue.
The next render after clicking the button shows something different:
Task 2: Ran after useEffect
Task 3: Ran after useEffect
Task 4: Ran after useEffect
Here it seems to have preemptively scheduled the task to process effects before the hooks were actually called.
Can anyone shed light on how it actually works?
Bonus Question
Strict mode is causing the component to render twice each time which is why we see duplicate entries for:
Microtask 2: Ran after useLayoutEffect
Microtask 3: Ran after useLayoutEffect
Microtask 4: Ran after useLayoutEffect
Why aren't there duplicate entries for these?
Synchronous 2: Ran after render
Synchronous 3: Ran after render
Synchronous 4: Ran after render