O gancho Make React useEffect não roda na renderização inicial

106

De acordo com a documentação:

componentDidUpdate()é chamado imediatamente após a atualização ocorrer. Este método não é chamado para a renderização inicial.

Podemos usar o novo useEffect()gancho para simular componentDidUpdate(), mas parece que useEffect()está sendo executado após cada renderização, mesmo na primeira vez. Como faço para que ele não execute na renderização inicial?

Como você pode ver no exemplo abaixo, componentDidUpdateFunctioné impresso durante a renderização inicial, mas componentDidUpdateClassnão foi impresso durante a renderização inicial.

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/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

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

Yangshun Tay
fonte
1
posso perguntar qual é o caso de uso em que faz sentido fazer algo com base no número de renderizações e não em uma variável de estado explícita como count?
abril

Respostas:

122

Podemos usar o useRefgancho para armazenar qualquer valor mutável que quisermos , então podemos usá-lo para rastrear se é a primeira vez que a useEffectfunção está sendo executada.

Se quisermos que o efeito seja executado na mesma fase que o componentDidUpdatefaz, podemos usar useLayoutEffect.

Exemplo

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/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

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

Tholle
fonte
5
Tentei substituir useRefcom useState, mas usar o setter acionou uma re-renderização, o que não acontece ao atribuir a, firstUpdate.currententão acho que essa é a única maneira legal :)
Aprillion
2
Alguém poderia explicar por que usar o efeito de layout se não estamos alterando ou medindo o DOM?
ZenVentzi
5
@ZenVentzi Não é necessário neste exemplo, mas a questão era como imitar componentDidUpdatecom ganchos, por isso usei.
Tholle
1
Criei um gancho personalizado aqui com base nesta resposta. Obrigado pela implementação!
Patrick Roberts
63

Você pode transformá-lo em ganchos personalizados , assim:

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;

Exemplo de uso:

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>
    );
}
// ...
Mehdi Dehghani
fonte
2
Essa abordagem lança avisos dizendo que a lista de dependências não é um literal de array.
theprogrammer
1
Eu uso este gancho em meus projetos e não vi nenhum aviso, você poderia fornecer mais informações?
Mehdi Dehghani
1
@vsync Você está pensando em um caso diferente em que deseja executar um efeito uma vez na renderização inicial e nunca mais
Equipe de programação
2
@vsync Na seção de notas de reactjs.org/docs/… diz especificamente "Se você quiser executar um efeito e limpá-lo apenas uma vez (na montagem e desmontagem), você pode passar um array vazio ([]) como um segundo argumento. " Isso corresponde ao comportamento observado para mim.
Programming Guy
10

Fiz um useFirstRendergancho simples para lidar com casos como focalizar uma entrada de formulário:

import { useRef, useEffect } from 'react';

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

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

  return firstRender.current;
}

Ele começa como e true, em seguida, muda para falseno useEffect, que é executado apenas uma vez e nunca mais.

Em seu componente, use-o:

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

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

Para o seu caso, você apenas usaria if (!firstRender) { ....

Marius marais
fonte
3

@ravi, o seu não chama a função de desmontagem passada. Esta é uma versão um pouco mais completa:

/**
 * 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
fonte
Olá @Whatabrain, como usar este gancho personalizado na passagem de lista não dependente? Não um vazio que seria o mesmo que componentDidmount, mas algo comouseEffect(() => {...});
KevDing
1
@KevDing Deve ser tão simples quanto omitir o dependenciesparâmetro ao chamá-lo.
Whatabrain
0

@MehdiDehghani, sua solução funciona perfeitamente bem, uma adição que você precisa fazer é desmontar e redefinir o didMount.currentvalor para false. Ao tentar usar este gancho personalizado em outro lugar, você não obtém o valor do cache.

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;
Ravi
fonte
2
Não tenho certeza se isso é necessário, pois se o componente acabar desmontando, porque se ele for remontado, didMount já terá sido reinicializado para false.
Cameron Yick