Сделайте так, чтобы перехватчик React useEffect не запускался при первоначальном рендеринге

95

Согласно документам:

componentDidUpdate()вызывается сразу после обновления. Этот метод не вызывается для первоначального рендеринга.

Мы можем использовать новый useEffect()хук для моделирования componentDidUpdate(), но похоже, что useEffect()он запускается после каждого рендеринга, даже в первый раз. Как мне заставить его не запускаться при начальном рендере?

Как вы можете видеть в приведенном ниже примере, componentDidUpdateFunctionон печатается во время первоначального рендеринга, но componentDidUpdateClassне печатается во время первоначального рендеринга.

function ComponentDidUpdateFunction() {
  const [count, setCount] = React.useState(0);
  React.useEffect(() => {
    console.log("componentDidUpdateFunction");
  });

  return (
    <div>
      <p>componentDidUpdateFunction: {count} times</p>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Click Me
      </button>
    </div>
  );
}

class ComponentDidUpdateClass extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  componentDidUpdate() {
    console.log("componentDidUpdateClass");
  }

  render() {
    return (
      <div>
        <p>componentDidUpdateClass: {this.state.count} times</p>
        <button
          onClick={() => {
            this.setState({ count: this.state.count + 1 });
          }}
        >
          Click Me
        </button>
      </div>
    );
  }
}

ReactDOM.render(
  <div>
    <ComponentDidUpdateFunction />
    <ComponentDidUpdateClass />
  </div>,
  document.querySelector("#app")
);
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>

Яншун Тай
источник
1
Могу я спросить, каков вариант использования, когда имеет смысл делать что-то на основе количества визуализаций, а не явной переменной состояния, например count?
Aprillion

Ответы:

111

Мы можем использовать useRefловушку для хранения любого изменяемого значения, которое нам нравится , поэтому мы можем использовать это, чтобы отслеживать useEffect, запускается ли функция в первый раз .

Если мы хотим, чтобы эффект выполнялся в той же фазе, что и componentDidUpdateон, мы можем использовать useLayoutEffectвместо этого.

пример

const { useState, useRef, useLayoutEffect } = React;

function ComponentDidUpdateFunction() {
  const [count, setCount] = useState(0);

  const firstUpdate = useRef(true);
  useLayoutEffect(() => {
    if (firstUpdate.current) {
      firstUpdate.current = false;
      return;
    }

    console.log("componentDidUpdateFunction");
  });

  return (
    <div>
      <p>componentDidUpdateFunction: {count} times</p>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Click Me
      </button>
    </div>
  );
}

ReactDOM.render(
  <ComponentDidUpdateFunction />,
  document.getElementById("app")
);
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>

Толле
источник
5
Я попытался заменить useRefна useState, но использование сеттера вызвало повторный рендеринг, чего не происходит при назначении, firstUpdate.currentпоэтому я думаю, что это единственный хороший способ :)
Aprillion
2
Может ли кто-нибудь объяснить, зачем использовать эффект макета, если мы не изменяем и не измеряем DOM?
ZenVentzi
5
@ZenVentzi В этом примере нет необходимости, но вопрос был в том, как имитировать componentDidUpdateс помощью хуков, поэтому я использовал его.
Tholle 06
Я создал пользовательский крюк здесь на основе этого ответа. Спасибо за реализацию!
Патрик Робертс
56

Вы можете превратить его в пользовательские хуки , например:

import React, { useEffect, useRef } from 'react';

const useDidMountEffect = (func, deps) => {
    const didMount = useRef(false);

    useEffect(() => {
        if (didMount.current) func();
        else didMount.current = true;
    }, deps);
}

export default useDidMountEffect;

Пример использования:

import React, { useState, useEffect } from 'react';

import useDidMountEffect from '../path/to/useDidMountEffect';

const MyComponent = (props) => {    
    const [state, setState] = useState({
        key: false
    });    

    useEffect(() => {
        // you know what is this, don't you?
    }, []);

    useDidMountEffect(() => {
        // react please run me if 'key' changes, but not on initial render
    }, [state.key]);    

    return (
        <div>
             ...
        </div>
    );
}
// ...
Мехди Дехгани
источник
2
Этот подход вызывает предупреждения о том, что список зависимостей не является литералом массива.
theprogrammer
1
Я использую этот хук в своих проектах и ​​не видел никаких предупреждений. Не могли бы вы предоставить дополнительную информацию?
Мехди Дехгани
1
@vsync Вы думаете о другом случае, когда вы хотите запустить эффект один раз при первоначальном рендеринге и никогда больше
Programming Guy
2
@vsync В разделе примечаний на сайте responsejs.org/docs/… конкретно сказано: «Если вы хотите запустить эффект и очистить его только один раз (при монтировании и размонтировании), вы можете передать пустой массив ([]) как второй аргумент ". Это соответствует наблюдаемому мной поведению.
Программист
5

Я сделал простой useFirstRenderкрючок для обработки таких случаев, как фокусирование ввода формы:

import { useRef, useEffect } from 'react';

export function useFirstRender() {
  const firstRender = useRef(true);

  useEffect(() => {
    firstRender.current = false;
  }, []);

  return firstRender.current;
}

Он начинается как true, затем переключается на falseв useEffect, который запускается только один раз и больше никогда.

В своем компоненте используйте его:

const firstRender = useFirstRender();
const phoneNumberRef = useRef(null);

useEffect(() => {
  if (firstRender || errors.phoneNumber) {
    phoneNumberRef.current.focus();
  }
}, [firstRender, errors.phoneNumber]);

В вашем случае вы просто используете if (!firstRender) { ....

Мариус Марэ
источник
3

@ravi, ваш не вызывает переданную функцию размонтирования. Вот версия, которая немного более полная:

/**
 * Identical to React.useEffect, except that it never runs on mount. This is
 * the equivalent of the componentDidUpdate lifecycle function.
 *
 * @param {function:function} effect - A useEffect effect.
 * @param {array} [dependencies] - useEffect dependency list.
 */
export const useEffectExceptOnMount = (effect, dependencies) => {
  const mounted = React.useRef(false);
  React.useEffect(() => {
    if (mounted.current) {
      const unmount = effect();
      return () => unmount && unmount();
    } else {
      mounted.current = true;
    }
  }, dependencies);

  // Reset on unmount for the next mount.
  React.useEffect(() => {
    return () => mounted.current = false;
  }, []);
};

Whatabrain
источник
Привет @Whatabrain, как использовать этот настраиваемый хук для передачи списка не зависимостей? Не пустой, как componentDidmount, а что-то вродеuseEffect(() => {...});
KevDing
1
@KevDing Должно быть так же просто, как опустить dependenciesпараметр при его вызове.
Whatabrain
1

@MehdiDehghani, ваше решение работает отлично, вам нужно сделать одно дополнение, это отключить, сбросить didMount.currentзначение на false. Когда пытаться использовать этот настраиваемый хук где-нибудь еще, вы не получите значение кеша.

import React, { useEffect, useRef } from 'react';

const useDidMountEffect = (func, deps) => {
    const didMount = useRef(false);

    useEffect(() => {
        let unmount;
        if (didMount.current) unmount = func();
        else didMount.current = true;

        return () => {
            didMount.current = false;
            unmount && unmount();
        }
    }, deps);
}

export default useDidMountEffect;
рави
источник
Я не уверен, что это необходимо, так как, если компонент все-таки размонтируется, потому что, если он будет перемонтирован, didMount уже будет повторно инициализирован false.
Кэмерон Йик,