How does React implement useEffect and useLayoutEffect?
Asked Answered
S

0

7

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 
Stove answered 27/8, 2021 at 1:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.