Hacer que React useEffect hook no se ejecuta en el render inicial

95

Según los documentos:

componentDidUpdate()se invoca inmediatamente después de que se realiza la actualización. Este método no se llama para el renderizado inicial.

Podemos usar el nuevo useEffect()gancho para simular componentDidUpdate(), pero parece que useEffect()se ejecuta después de cada renderizado, incluso la primera vez. ¿Cómo hago para que no se ejecute en el renderizado inicial?

Como puede ver en el ejemplo siguiente, componentDidUpdateFunctionse imprime durante el renderizado inicial pero componentDidUpdateClassno se imprimió durante el renderizado 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
fuente
1
¿Puedo preguntar cuál es el caso de uso cuando tiene sentido hacer algo basado en el número de representaciones y no en una variable de estado explícita como count?
Aprillion

Respuestas:

111

Podemos usar el useRefgancho para almacenar cualquier valor mutable que nos guste , por lo que podríamos usarlo para realizar un seguimiento de si es la primera vez useEffectque se ejecuta la función.

Si queremos que el efecto se ejecute en la misma fase que lo componentDidUpdatehace, podemos usar useLayoutEffecten su lugar.

Ejemplo

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
fuente
5
Traté de sustituir useRefcon useState, pero utilizando la incubadora provocó una re-render, que no está sucediendo cuando se asigna a firstUpdate.currentasí que supongo que esta es la única manera agradable :)
Aprillion
2
¿Alguien podría explicar por qué usar el efecto de diseño si no estamos mutando o midiendo el DOM?
ZenVentzi
5
@ZenVentzi No es necesario en este ejemplo, pero la pregunta era cómo imitar componentDidUpdatecon ganchos, por eso lo usé.
Tholle
Creé un gancho personalizado aquí basado en esta respuesta. ¡Gracias por la implementación!
Patrick Roberts
56

Puede convertirlo en ganchos personalizados , así:

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;

Ejemplo 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
fuente
2
Este enfoque arroja advertencias que dicen que la lista de dependencias no es un literal de matriz.
theprogrammer
1
Utilizo este gancho en mis proyectos y no vi ninguna advertencia, ¿podrían proporcionar más información?
Mehdi Dehghani
1
@vsync Estás pensando en un caso diferente en el que quieres ejecutar un efecto una vez en el renderizado inicial y nunca más
Programming Guy
2
@vsync En la sección de notas de reactjs.org/docs/… dice específicamente "Si desea ejecutar un efecto y limpiarlo solo una vez (al montar y desmontar), puede pasar una matriz vacía ([]) como segundo argumento ". Esto coincide con el comportamiento observado para mí.
Programming Guy
5

Hice un useFirstRendergancho simple para manejar casos como enfocar una entrada de formulario:

import { useRef, useEffect } from 'react';

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

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

  return firstRender.current;
}

Comienza como true, luego cambia a falseen useEffect, que solo se ejecuta una vez y nunca más.

En su componente, utilícelo:

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

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

Para su caso, solo usaría if (!firstRender) { ....

Marius Marais
fuente
3

@ravi, el tuyo no llama a la función de desmontaje pasada. Aquí tienes una versión un poco más 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
fuente
Hola @Whatabrain, ¿cómo usar este gancho personalizado para pasar la lista de no dependencia? No un vacío que sería lo mismo que componentDidmount, sino algo comouseEffect(() => {...});
KevDing
1
@KevDing Debería ser tan simple como omitir el dependenciesparámetro cuando lo llama.
Whatabrain
1

@MehdiDehghani, su solución funciona perfectamente bien, una adición que debe hacer es desmontar, restablecer el didMount.currentvalor a false. Cuando intente usar este gancho personalizado en otro lugar, no obtendrá el valor de la caché.

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
fuente
No estoy seguro de que esto sea necesario, ya que si el componente termina desmontando, porque si se vuelve a montar, didMount ya se reinicializará en false.
Cameron Yick