Spraw, aby hak React useEffect nie działał przy początkowym renderowaniu

106

Według dokumentów:

componentDidUpdate()jest wywoływana natychmiast po wystąpieniu aktualizacji. Ta metoda nie jest wywoływana przy pierwszym renderowaniu.

Możemy użyć nowego useEffect()hooka do symulacji componentDidUpdate(), ale wygląda na to, że useEffect()jest uruchamiany po każdym renderowaniu, nawet za pierwszym razem. Jak sprawić, by nie działał przy początkowym renderowaniu?

Jak widać w poniższym przykładzie, componentDidUpdateFunctionjest drukowany podczas pierwszego renderowania, ale componentDidUpdateClassnie został wydrukowany podczas pierwszego renderowania.

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
źródło
1
czy mogę zapytać, jaki jest przypadek użycia, gdy ma sens zrobienie czegoś na podstawie liczby renderów, a nie jawnej zmiennej stanu, takiej jak count?
Kwietnia

Odpowiedzi:

122

Możemy użyć useRefhaka do przechowywania dowolnej zmiennej wartości, którą lubimy , więc możemy użyć tego do śledzenia, czy jest to pierwsze uruchomienie useEffectfunkcji.

Jeśli chcemy, aby efekt działał w tej samej fazie co componentDidUpdateto, możemy useLayoutEffectzamiast tego użyć .

Przykład

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
źródło
5
Starałem się wymienić useRefz useState, ale przy użyciu setter wyzwalane re-render, który się nie dzieje, gdy przypisanie firstUpdate.currentwięc myślę, że jest to jedyny dobry sposób :)
Aprillion
2
Czy ktoś mógłby wyjaśnić, po co używać efektu układu, jeśli nie mutujemy ani nie mierzymy DOM?
ZenVentzi
5
@ZenVentzi W tym przykładzie nie jest to konieczne, ale pytanie brzmiało, jak naśladować za componentDidUpdatepomocą hooków, dlatego go użyłem.
Tholle
1
I stworzył hak niestandardową tutaj na podstawie tej odpowiedzi. Dzięki za realizację!
Patrick Roberts
64

Możesz zmienić go w niestandardowe haczyki , na przykład:

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;

Przykład użycia:

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
źródło
2
Takie podejście generuje ostrzeżenia informujące, że lista zależności nie jest literałem tablicowym.
theprogrammer
1
Używam tego haka w moich projektach i nie widziałem żadnego ostrzeżenia, czy możesz podać więcej informacji?
Mehdi Dehghani
1
@vsync Myślisz o innym przypadku, w którym chcesz uruchomić efekt raz przy pierwszym renderowaniu i nigdy więcej
Programming Guy
2
@vsync W sekcji notatek na stronie internetowej awarejs.org/docs/ jest napisane „Jeśli chcesz uruchomić efekt i wyczyścić go tylko raz (podczas montowania i odmontowywania), możesz przekazać pustą tablicę ([]) jako drugi argument. " To pasuje do obserwowanego zachowania u mnie.
Programming Guy
10

Zrobiłem prosty useFirstRenderhaczyk do obsługi przypadków, takich jak skupianie się na danych wejściowych formularza:

import { useRef, useEffect } from 'react';

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

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

  return firstRender.current;
}

Zaczyna się jako true, a następnie przełącza się na falsew useEffect, który działa tylko raz i nigdy więcej.

W swoim komponencie użyj go:

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

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

W twoim przypadku po prostu użyjesz if (!firstRender) { ....

Marius Marais
źródło
3

@ravi, yours nie wywołuje przekazanej funkcji odmontowania. Oto trochę bardziej kompletna wersja:

/**
 * 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
źródło
Witaj @Whatabrain, jak używać tego niestandardowego hooka do przekazywania listy niezależności? Nie pusty, który byłby taki sam jak componentDidmount, ale coś w rodzajuuseEffect(() => {...});
KevDing
1
@KevDing Powinno być tak proste, jak pominięcie dependenciesparametru podczas jego wywoływania.
Whatabrain
0

@MehdiDehghani, twoje rozwiązanie działa idealnie, jeden dodatek, który musisz zrobić, to odmontować, zresetować didMount.currentwartość do false. Kiedy próbować użyć tego niestandardowego zaczepu w innym miejscu, nie otrzymasz wartości pamięci podręcznej.

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
źródło
2
Nie jestem pewien, czy jest to konieczne, ponieważ jeśli komponent zostanie odmontowany, ponieważ jeśli zostanie zamontowany ponownie, didMount zostanie już ponownie zainicjowany false.
Cameron Yick