Make React useEffect-Hook wird beim ersten Rendern nicht ausgeführt

95

Laut den Dokumenten:

componentDidUpdate()wird sofort nach der Aktualisierung aufgerufen. Diese Methode wird beim ersten Rendern nicht aufgerufen.

Wir können den neuen useEffect()Hook zum Simulieren verwenden componentDidUpdate(), aber es scheint, useEffect()als würde er nach jedem Rendern ausgeführt, sogar beim ersten Mal. Wie kann ich erreichen, dass es beim ersten Rendern nicht ausgeführt wird?

Wie Sie im folgenden Beispiel sehen können, componentDidUpdateFunctionwird es beim ersten Rendern componentDidUpdateClassgedruckt , wurde jedoch beim ersten Rendern nicht gedruckt.

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
quelle
1
Darf ich fragen, was der Anwendungsfall ist, wenn es sinnvoll ist, etwas basierend auf der Anzahl der Renderings und nicht einer expliziten Statusvariablen wie zu tun count?
Aprillion

Antworten:

111

Wir können den useRefHook verwenden, um einen beliebigen veränderlichen Wert zu speichern , damit wir verfolgen können, ob die useEffectFunktion zum ersten Mal ausgeführt wird.

Wenn der Effekt in derselben Phase ausgeführt werden componentDidUpdatesoll, können wir ihn useLayoutEffectstattdessen verwenden.

Beispiel

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
quelle
5
Ich habe versucht , ersetzen useRefmit useState, aber die Setter mit ausgelöst einem erneuten machen, das ist nicht der Fall , wenn auf der Zuordnung firstUpdate.currentso dass ich denke , das ist die einzige nette Weise ist :)
Aprillion
2
Könnte jemand erklären, warum der Layout-Effekt verwendet wird, wenn wir das DOM nicht mutieren oder messen?
ZenVentzi
5
@ZenVentzi In diesem Beispiel ist es nicht notwendig, aber die Frage war, wie man componentDidUpdatemit Haken nachahmt , deshalb habe ich es verwendet.
Tholle
Ich habe einen benutzerdefinierten Haken hier auf der Grundlage dieser Antwort. Danke für die Implementierung!
Patrick Roberts
56

Sie können es wie folgt in benutzerdefinierte Haken verwandeln :

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;

Anwendungsbeispiel:

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
quelle
2
Dieser Ansatz gibt Warnungen aus, die besagen, dass die Abhängigkeitsliste kein Array-Literal ist.
Programmierer
1
Ich verwende diesen Haken in meinen Projekten und habe keine Warnung gesehen. Können Sie weitere Informationen bereitstellen?
Mehdi Dehghani
1
@vsync Sie denken an einen anderen Fall, in dem Sie einen Effekt einmal beim ersten Rendern und nie wieder ausführen möchten
Programming Guy
2
@vsync Im Abschnitt " Notizen" von reactjs.org/docs/… heißt es ausdrücklich: "Wenn Sie einen Effekt ausführen und nur einmal bereinigen möchten (beim Ein- und Aushängen), können Sie ein leeres Array ([]) als übergeben zweites Argument. " Dies entspricht dem beobachteten Verhalten für mich.
Programming Guy
5

Ich habe einen einfachen useFirstRenderHook erstellt, um Fälle wie das Fokussieren einer Formulareingabe zu behandeln:

import { useRef, useEffect } from 'react';

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

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

  return firstRender.current;
}

Es beginnt als true, wechselt dann zu falsein das useEffect, das nur einmal und nie wieder läuft.

Verwenden Sie es in Ihrer Komponente:

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

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

Für Ihren Fall würden Sie nur verwenden if (!firstRender) { ....

Marius Marais
quelle
3

@ravi, deine ruft die übergebene Unmount-Funktion nicht auf. Hier ist eine etwas vollständigere Version:

/**
 * 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
quelle
Hallo @Whatabrain, wie wird dieser benutzerdefinierte Hook zum Übergeben einer Liste ohne Abhängigkeiten verwendet? Kein useEffect(() => {...});
Leerzeichen
1
@KevDing Sollte so einfach sein, dependenciesdass Sie den Parameter beim Aufrufen weglassen .
Whatabrain
0

@MehdiDehghani, Ihre Lösung funktioniert einwandfrei. Eine Ergänzung, die Sie tun müssen, ist das Aufheben der Bereitstellung und das Zurücksetzen des didMount.currentWerts auf false. Wenn Sie versuchen, diesen benutzerdefinierten Hook an einer anderen Stelle zu verwenden, erhalten Sie keinen Cache-Wert.

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
quelle
1
Ich bin mir nicht sicher, ob dies erforderlich ist, da didMount bereits neu initialisiert wird, wenn die Komponente nicht bereitgestellt wird. Wenn sie erneut bereitgestellt wird, wird didMount bereits neu initialisiert false.
Cameron Yick