Jadikan hook React useEffect tidak berjalan pada render awal

95

Menurut dokumen:

componentDidUpdate()dipanggil segera setelah pembaruan terjadi. Metode ini tidak dipanggil untuk render awal.

Kita bisa menggunakan useEffect()hook baru untuk mensimulasikan componentDidUpdate(), tetapi sepertinya useEffect()dijalankan setelah setiap render, bahkan untuk pertama kalinya. Bagaimana cara membuatnya tidak berjalan pada render awal?

Seperti yang Anda lihat pada contoh di bawah ini, componentDidUpdateFunctiondicetak selama render awal tetapi componentDidUpdateClasstidak dicetak selama render awal.

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
sumber
1
bolehkah saya bertanya apa use case ketika masuk akal untuk melakukan sesuatu berdasarkan jumlah render dan bukan variabel keadaan eksplisit seperti count?
Aprillion

Jawaban:

111

Kita bisa menggunakan useRefhook untuk menyimpan nilai yang bisa berubah yang kita suka , jadi kita bisa menggunakannya untuk melacak apakah ini pertama kalinya useEffectfungsi dijalankan.

Jika kita ingin efeknya berjalan dalam fase yang componentDidUpdatesama, kita bisa menggunakannya useLayoutEffect.

Contoh

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
sumber
5
Saya mencoba menggantinya useRefdengan useState, tetapi menggunakan setter memicu render ulang, yang tidak terjadi saat menugaskan firstUpdate.currentjadi saya kira ini adalah satu-satunya cara yang bagus :)
Aprillion
2
Bisakah seseorang menjelaskan mengapa menggunakan efek tata letak jika kita tidak melakukan mutasi atau pengukuran DOM?
ZenVentzi
5
@ZenVentzi Ini tidak perlu dalam contoh ini, tetapi pertanyaannya adalah bagaimana meniru componentDidUpdatedengan kait, jadi itulah mengapa saya menggunakannya.
Tholle
Saya membuat pengait khusus di sini berdasarkan jawaban ini. Terima kasih atas implementasinya!
Patrick Roberts
56

Anda dapat mengubahnya menjadi pengait khusus , seperti:

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;

Contoh penggunaan:

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
sumber
2
Pendekatan ini melempar peringatan yang mengatakan bahwa daftar ketergantungan bukan literal array.
programmer
1
Saya menggunakan pengait ini dalam proyek saya dan saya tidak melihat peringatan apa pun, dapatkah Anda memberikan info lebih lanjut?
Mehdi Dehghani
1
@vsync Anda memikirkan tentang kasus berbeda di mana Anda ingin menjalankan efek sekali pada render awal dan tidak pernah lagi
Pemrograman
2
@vsync Di bagian catatan reactjs.org/docs/… secara spesifik mengatakan "Jika Anda ingin menjalankan efek dan membersihkannya hanya sekali (pada mount dan unmount), Anda dapat meneruskan array kosong ([]) sebagai argumen kedua. " Ini cocok dengan perilaku yang diamati untuk saya.
Pemrogram
5

Saya membuat useFirstRenderpengait sederhana untuk menangani kasus seperti memfokuskan input formulir:

import { useRef, useEffect } from 'react';

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

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

  return firstRender.current;
}

Ini dimulai sebagai true, kemudian beralih ke falsedalam useEffect, yang hanya berjalan sekali, dan tidak pernah lagi.

Di komponen Anda, gunakan:

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

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

Untuk kasus Anda, Anda hanya akan menggunakan if (!firstRender) { ....

Marius Marais
sumber
3

@ravi, milik Anda tidak memanggil fungsi pelepasan yang diteruskan. Ini versi yang sedikit lebih lengkap:

/**
 * 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;
  }, []);
};

Otak apa
sumber
Halo @Whatabrain, bagaimana cara menggunakan hook khusus ini untuk meneruskan daftar non-ketergantungan? Bukan kosong yang akan sama dengan componentDidmount tetapi sesuatu sepertiuseEffect(() => {...});
KevDing
1
@KevDing Harus sesederhana menghilangkan dependenciesparameter saat Anda memanggilnya.
Whatabrain
0

@MehdiDehghani, solusi Anda berfungsi dengan baik, satu tambahan yang harus Anda lakukan adalah di unmount, setel ulang didMount.currentnilainya ke false. Saat mencoba menggunakan hook khusus ini di tempat lain, Anda tidak mendapatkan nilai 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
sumber
1
Saya tidak yakin ini perlu, karena jika komponen akhirnya dilepas, karena jika dipasang ulang, didMount sudah akan diinisialisasi ulang ke false.
Cameron Yick