React Hook has been around for a while now, and I’m sure many people already have hooks in their code. UseEffect and useLayoutEffect are the ones we use the most. So what’s the difference between them?

use

The way these two functions are used is very simple. They both take a function and an array and only execute effect if the values in the array change. So for the use of the way I did not introduce more, not clear can first refer to the official website.

differences

  • useEffectIs executed asynchronously, whileuseLayoutEffectIt is executed synchronously.
  • useEffectIs executed after the browser has finished rendering, anduseLayoutEffectIs executed before the browser actually renders the content into the interface, andcomponentDidMountEquivalence.

The specific performance

Let’s take a very simple example

import React, { useEffect, useLayoutEffect, useState } from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  const [state, setState] = useState("hello world")

  useEffect(() = > {
    let i = 0;
    while(i <= 100000000) {
      i++;
    };
    setState("world hello"); } []);// useLayoutEffect(() => {
  // let i = 0;
  // while(i <= 100000000) {
  // i++;
  / /};
  // setState("world hello");
  / /} []);

  return (
    <>
      <div>{state}</div>
    </>
  );
}

export default App;
Copy the code

This is what it does

With the useLayoutEffect, the flicker disappeared

I’m sure you can understand the difference, because useEffect is executed asynchronously after rendering, which causes Hello World to be rendered on the screen first, and then world Hello to blink. UseLayoutEffect is executed synchronously before rendering, so it waits until it finishes rendering to avoid flicker. In other words, it is best to put the dom manipulation operations in the useLayouteEffect to avoid flickering.

ssr

Because useLayoutEffect may render different relationships, you will get a warning if you use this function in SSR.

Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://fb.me/react-uselayouteffect-ssr for common fixes.
Copy the code

This is because useLayoutEffect is not executed on the server, so it is possible that the CONTENT rendered by SSR will not match the actual first screen content. And the solution is simple:

  1. Give up usinguseLayoutEffect, the use ofuseEffectInstead of
  2. If you know for sureuseLayouteffectIt doesn’t affect the first rendering, but it will be needed later, so you can write it like this
import { useEffect, useLayoutEffect } from 'react';
export const useCustomLayoutEffect = typeof window! = ='undefined' ? useLayoutEffect : useEffect;
Copy the code

Use useCustomLayoutEffect instead when you use useLayoutEffect. UseEffect is used on the server so that no warning is reported.

Source analysis

So when are useEffect and useLayoutEffect actually called? Let’s take a look at the source code.

useEffect

Start by finding the entry to the useEffect call

function updateEffect(create, deps) {{// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
    if ('undefined'! = =typeof jest) {
      warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber$1); }}return updateEffectImpl(Update | Passive, Passive$1, create, deps);
}
Copy the code

When updateEffectImpl is called, the hookEffectTag is passed as Passive$1, so let’s look for Passive$1.

We then find that this is where we pass in the Passive$1 type to call useEffect.

function commitPassiveHookEffects(finishedWork) {
  if((finishedWork.effectTag & Passive) ! == NoEffect) {switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent:
      case Block:
        {
          // TODO (#17945) We should call all passive destroy functions (for all fibers)
          // before calling any create functions. The current approach only serializes
          // these for a single fiber.
          commitHookEffectListUnmount(Passive$1 | HasEffect, finishedWork);
          commitHookEffectListMount(Passive$1 | HasEffect, finishedWork);
          break; }}}}Copy the code

Then we continue to search for committee passiveHookeffects

function flushPassiveEffectsImpl() {... omitwhile(_effect2 ! = =null) {
      {
        setCurrentFiber(_effect2);
        invokeGuardedCallback(null, commitPassiveHookEffects, null, _effect2);
      }
   }
    ...省略
}
Copy the code

Same old, flushPassiveEffectsImpl

function flushPassiveEffects() {
  if(pendingPassiveEffectsRenderPriority ! == NoPriority) {var priorityLevel = pendingPassiveEffectsRenderPriority > NormalPriority ? NormalPriority : pendingPassiveEffectsRenderPriority;
    pendingPassiveEffectsRenderPriority = NoPriority;
    return runWithPriority$1(priorityLevel, flushPassiveEffectsImpl); }}Copy the code

Up a layer is commitBeforeMutationEffects again, it is call the method that flushPassiveEffects scheduleCallback, this is a scheduling operations, is executed asynchronously.

function commitBeforeMutationEffects{... omitif((effectTag & Passive) ! == NoEffect) {// If there are passive effects, schedule a callback to flush at
      // the earliest opportunity.
      if(! rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects =true;
        scheduleCallback(NormalPriority, function () {
          flushPassiveEffects();
          return null;
        });
      }
    }
    ...省略
}
Copy the code

Continue to follow up commitBeforeMutationEffects method to find, we can find the final call is commitRootImpl useEffect place, this is we commit phase will call a function, So this is where the useEffect is scheduled and asynchronously executed after rendering.

useLayoutEffect

As usual, start at the entrance

function updateLayoutEffect(create, deps) {
  return updateEffectImpl(Update, Layout, create, deps);
}
Copy the code

So the hookEffectTag that’s passed in here is Layout, so let’s look for Layout.

function commitLifeCycles(finishedRoot, current, finishedWork, committedExpirationTime) {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent:
    case Block:
      {
        // At this point layout effects have already been destroyed (during mutation phase).
        // This is done to prevent sibling component effects from interfering with each other,
        // e.g. a destroy function in one component should never override a ref set
        // by a create function in another component during the same commit.
        commitHookEffectListMount(Layout | HasEffect, finishedWork);

        return;
      }

    case ClassComponent:
      {
        var instance = finishedWork.stateNode;

        if (finishedWork.effectTag & Update) {
          if (current === null) {
            startPhaseTimer(finishedWork, 'componentDidMount'); // We could update instance props and state here,
            // but instead we rely on them being set during last render.
            // TODO: revisit this when we implement resuming.

            {
              if(finishedWork.type === finishedWork.elementType && ! didWarnAboutReassigningProps) {if(instance.props ! == finishedWork.memoizedProps) { error('Expected %s props to match memoized props before ' + 'componentDidMount. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentName(finishedWork.type) || 'instance');
                }

                if(instance.state ! == finishedWork.memoizedState) { error('Expected %s state to match memoized state before ' + 'componentDidMount. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentName(finishedWork.type) || 'instance');
                }
              }
            }

            instance.componentDidMount();
            stopPhaseTimer();
          } else {
            var prevProps = finishedWork.elementType === finishedWork.type ? current.memoizedProps : resolveDefaultProps(finishedWork.type, current.memoizedProps);
            var prevState = current.memoizedState;
            startPhaseTimer(finishedWork, 'componentDidUpdate'); // We could update instance props and state here,
            // but instead we rely on them being set during last render.
            // TODO: revisit this when we implement resuming.

            {
              if(finishedWork.type === finishedWork.elementType && ! didWarnAboutReassigningProps) {if(instance.props ! == finishedWork.memoizedProps) { error('Expected %s props to match memoized props before ' + 'componentDidUpdate. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentName(finishedWork.type) || 'instance');
                }

                if(instance.state ! == finishedWork.memoizedState) { error('Expected %s state to match memoized state before ' + 'componentDidUpdate. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentName(finishedWork.type) || 'instance');
                }
              }
            }

            instance.componentDidUpdate(prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate);
            stopPhaseTimer();
          }
        }
      ...省略
}
Copy the code

UseLayoutEffect is equivalent to componentDidMount. For componentDidMount, useLayoutEffect is componentDidMount.

Searching all the way up will eventually find the commitRootImpl method, and no scheduling method is found, so useLayoutEffect is executed synchronously.

conclusion

  1. Preferred to useuseEffect, because it is executed asynchronously and does not block rendering
  2. Will affect the rendering operation as far as possibleuseLayoutEffectTo avoid flicker problems
  3. useLayoutEffectandcomponentDidMountIs equivalent, will call synchronously, blocking rendering
  4. useLayoutEffectA warning is issued when used for server rendering because it may cause inconsistency between the actual first screen and the server rendering.