Loop tak terbatas dalam useEffect

115

Saya telah bermain-main dengan sistem hook baru di React 16.7-alpha dan terjebak dalam loop tak terbatas dalam useEffect ketika keadaan yang saya tangani adalah objek atau array.

Pertama, saya menggunakan useState dan memulainya dengan objek kosong seperti ini:

const [obj, setObj] = useState({});

Kemudian, dalam useEffect, saya menggunakan setObj untuk menyetelnya ke objek kosong lagi. Sebagai argumen kedua saya meneruskan [obj], berharap itu tidak akan diperbarui jika konten objek tidak berubah. Tapi itu terus diperbarui. Saya rasa karena apa pun isinya, objek ini selalu berbeda sehingga membuat React berpikir itu terus berubah?

useEffect(() => {
  setIngredients({});
}, [ingredients]);

Hal yang sama berlaku dengan array, tetapi sebagai primitif, ia tidak akan terjebak dalam loop, seperti yang diharapkan.

Dengan menggunakan kait baru ini, bagaimana saya harus menangani objek dan larik saat memeriksa cuaca, konten telah berubah atau tidak?

Tobias Haugen
sumber
Tobias, kasus penggunaan apa yang membutuhkan perubahan nilai bahan, begitu nilainya berubah?
Ben Carp
@Tobias, Anda harus membaca jawaban saya. Saya yakin Anda akan menerimanya sebagai jawaban yang benar.
HalfWebDev

Jawaban:

128

Meneruskan array kosong sebagai argumen kedua ke useEffect membuatnya hanya berjalan pada mount dan unmount, sehingga menghentikan loop tak terbatas.

useEffect(() => {
  setIngredients({});
}, []);

Ini diklarifikasi kepada saya dalam posting blog di React hooks di https://www.robinwieruch.de/react-hooks/

Tobias Haugen
sumber
1
Ini sebenarnya adalah array kosong, bukan objek kosong yang harus Anda lewati.
GifCo
20
Ini tidak menyelesaikan masalah, Anda harus meneruskan dependensi yang digunakan oleh hook
helado
1
Perhatikan bahwa saat melepas efeknya akan menjalankan fungsi pembersihan jika Anda telah menentukannya. Efek sebenarnya tidak berjalan saat dilepas. reactjs.org/docs/hooks-effect.html#example-using-hooks-1
tony
2
Menggunakan array kosong ketika setIngrediets adalah ketergantungan adalah antipattern seperti yang dikatakan Dan Abramov. Jangan perlakukan useEffetct seperti metode componentDidMount ()
p7adams
1
karena orang-orang di atas tidak memberikan tautan atau sumber daya tentang cara menyelesaikan masalah ini dengan benar, saya sarankan pengunjung duckduckgo yang akan datang untuk memeriksanya, karena ini memperbaiki masalah loop tak terbatas saya: stackoverflow.com/questions/56657907/…
C. Rib
78

Punya masalah yang sama. Saya tidak tahu mengapa mereka tidak menyebutkan ini di dokumen. Hanya ingin menambahkan sedikit pada jawaban Tobias Haugen.

Untuk berjalan di setiap komponen / perenderan induk, Anda perlu menggunakan:

  useEffect(() => {

    // don't know where it can be used :/
  })

Untuk menjalankan apa pun hanya satu kali setelah pemasangan komponen (akan dirender sekali), Anda perlu menggunakan:

  useEffect(() => {

    // do anything only one time if you pass empty array []
    // keep in mind, that component will be rendered one time (with default values) before we get here
  }, [] )

Untuk menjalankan apa pun satu kali pada pemasangan komponen dan pada perubahan data / data2 :

  const [data, setData] = useState(false)
  const [data2, setData2] = useState('default value for first render')
  useEffect(() => {

// if you pass some variable, than component will rerender after component mount one time and second time if this(in my case data or data2) is changed
// if your data is object and you want to trigger this when property of object changed, clone object like this let clone = JSON.parse(JSON.stringify(data)), change it clone.prop = 2 and setData(clone).
// if you do like this 'data.prop=2' without cloning useEffect will not be triggered, because link to data object in momory doesn't changed, even if object changed (as i understand this)
  }, [data, data2] )

Bagaimana saya menggunakannya di sebagian besar waktu:

export default function Book({id}) { 
  const [book, bookSet] = useState(false) 

  useEffect(() => {
    loadBookFromServer()
  }, [id]) // every time id changed, new book will be loaded

  // Remeber that it's not always safe to omit functions from the list of dependencies. Explained in comments.
  async function loadBookFromServer() {
    let response = await fetch('api/book/' + id)
    response  = await response.json() 
    bookSet(response)
  }

  if (!book) return false //first render, when useEffect did't triggered yet we will return false

  return <div>{JSON.stringify(book)}</div>  
}
ZiiMakc
sumber
@endavid tergantung pada prop apa yang Anda gunakan
ZiiMakc
1
tentu saja, jika Anda tidak menggunakan nilai apa pun dari lingkup komponen, maka aman untuk dihilangkan. Tapi dari titik vue perancangan / arsitektur murni, ini bukan praktik yang baik, karena ini mengharuskan Anda untuk memindahkan seluruh fungsi Anda ke dalam efek jika Anda perlu menggunakan props dan Anda bisa berakhir dengan metode useEffect yang akan menggunakan jumlah baris kode yang keterlaluan.
egdavid
18

Saya mengalami masalah yang sama juga sekali dan saya memperbaikinya dengan memastikan saya meneruskan nilai primitif di argumen kedua [].

Jika Anda melewatkan suatu objek, React hanya akan menyimpan referensi ke objek tersebut dan menjalankan efeknya ketika referensi berubah, yang biasanya terjadi setiap saat (saya tidak tahu bagaimana caranya).

Solusinya adalah dengan meneruskan nilai pada objek. Anda dapat mencoba,

const obj = { keyA: 'a', keyB: 'b' }

useEffect(() => {
  // do something
}, [Object.values(obj)]);

atau

const obj = { keyA: 'a', keyB: 'b' }

useEffect(() => {
  // do something
}, [obj.keyA, obj.keyB]);
Dinesh Pandiyan
sumber
4
Pendekatan lain adalah membuat nilai seperti itu dengan useMemo, dengan cara itu referensi disimpan dan nilai dependensi dievaluasi sama
helado
@helado Anda dapat menggunakan useMemo () untuk nilai atau useCallback () untuk fungsi
Juanma Menendez
Apakah nilai acak itu diteruskan ke larik ketergantungan? Tentu saja ini mencegah loop tak terbatas tetapi apakah ini didorong? Saya bisa lolos 0ke array ketergantungan juga?
thinkvantagedu
11

Seperti yang dikatakan dalam dokumentasi ( https://reactjs.org/docs/hooks-effect.html ), useEffecthook dimaksudkan untuk digunakan ketika Anda ingin beberapa kode dieksekusi setelah setiap render . Dari dokumen:

Apakah useEffect berjalan setelah setiap render? Iya!

Jika Anda ingin menyesuaikan ini, Anda dapat mengikuti petunjuk yang muncul nanti di halaman yang sama ( https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects ). Pada dasarnya, useEffectmetode menerima argumen kedua , yang akan diperiksa oleh React untuk menentukan apakah efeknya harus dipicu lagi atau tidak.

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

Anda dapat mengirimkan objek apa pun sebagai argumen kedua. Jika objek ini tetap tidak berubah, efek Anda hanya akan dipicu setelah pemasangan pertama . Jika objek berubah, efeknya akan dipicu lagi.

Rocío García Luque
sumber
11

Jika Anda membuat pengait khusus , terkadang Anda dapat menyebabkan pengulangan tak terbatas dengan default sebagai berikut

function useMyBadHook(values = {}) {
    useEffect(()=> { 
           /* This runs every render, if values is undefined */
        },
        [values] 
    )
}

Cara mengatasinya adalah dengan menggunakan objek yang sama daripada membuat yang baru pada setiap pemanggilan fungsi:

const defaultValues = {};
function useMyBadHook(values = defaultValues) {
    useEffect(()=> { 
           /* This runs on first call and when values change */
        },
        [values] 
    )
}

Jika Anda mengalami ini di kode komponen Anda , loop mungkin diperbaiki jika Anda menggunakan defaultProps daripada nilai default ES6

function MyComponent({values}) {
  useEffect(()=> { 
       /* do stuff*/
    },[values] 
  )
  return null; /* stuff */
}

MyComponent.defaultProps = {
  values = {}
}
Jkarttunen
sumber
Terima kasih! Ini tidak jelas bagi saya dan sebenarnya masalah yang saya hadapi.
stuckj
1
Anda menyelamatkan hari saya! Makasih bro.
Han Van Pham
7

Saya tidak yakin apakah ini akan berhasil untuk Anda, tetapi Anda dapat mencoba menambahkan .length seperti ini:

useEffect(() => {
        // fetch from server and set as obj
}, [obj.length]);

Dalam kasus saya (saya sedang mengambil sebuah array!) Itu mengambil data di mount, kemudian hanya pada perubahan dan tidak masuk ke dalam loop.

Luka M
sumber
1
Bagaimana jika sebuah item diganti dalam array? Dalam hal ini, panjang array akan sama dan efeknya tidak akan berjalan!
Aamir Khan
7

Jika Anda menyertakan array kosong di akhir useEffect :

useEffect(()=>{
        setText(text);
},[])

Ini akan berjalan sekali.

Jika Anda menyertakan juga parameter pada array:

useEffect(()=>{
            setText(text);
},[text])

Ini akan berjalan setiap kali parameter teks berubah.

besartm
sumber
mengapa itu akan berjalan hanya sekali jika kita meletakkan array kosong di ujung hook?
Karen
Array kosong di akhir useEffect adalah implementasi yang disengaja oleh pengembang untuk menghentikan loop tak terbatas dalam situasi di mana Anda mungkin, misalnya, perlu setState di dalam useEffect. Ini sebaliknya akan mengarah ke useEffect -> update status -> useEffect -> loop tak terbatas.
to240
Array kosong menyebabkan peringatan dari eslinter.
hmcclungiii
4

Putaran tak terbatas Anda disebabkan oleh sirkularitas

useEffect(() => {
  setIngredients({});
}, [ingredients]);

setIngredients({});akan mengubah nilai ingredients(akan mengembalikan referensi baru setiap kali), yang akan dijalankan setIngredients({}). Untuk mengatasi ini, Anda dapat menggunakan salah satu pendekatan:

  1. Teruskan argumen kedua yang berbeda ke useEffect
const timeToChangeIngrediants = .....
useEffect(() => {
  setIngredients({});
}, [timeToChangeIngrediants ]);

setIngrediantsakan berjalan jika timeToChangeIngrediantstelah berubah.

  1. Saya tidak yakin kasus penggunaan apa yang membenarkan bahan perubahan setelah diubah. Namun jika demikian, Anda meneruskan Object.values(ingrediants)sebagai argumen kedua ke useEffect.
useEffect(() => {
  setIngredients({});
}, Object.values(ingrediants));
Ben Carp
sumber
2

Jika Anda menggunakan pengoptimalan ini, pastikan larik menyertakan semua nilai dari cakupan komponen (seperti props dan status) yang berubah seiring waktu dan yang digunakan oleh efeknya.

Saya yakin mereka mencoba untuk mengungkapkan kemungkinan bahwa seseorang dapat menggunakan data lama, dan untuk mewaspadai hal ini. Tidak masalah jenis nilai yang kita kirimkan arrayuntuk argumen kedua selama kita tahu bahwa jika salah satu dari nilai-nilai itu berubah, itu akan mengeksekusi efeknya. Jika kita menggunakan ingredientssebagai bagian dari penghitungan dalam efek, kita harus memasukkannya ke dalam array.

const [ingredients, setIngredients] = useState({});

// This will be an infinite loop, because by shallow comparison ingredients !== {} 
useEffect(() => {
  setIngredients({});
}, [ingredients]);

// If we need to update ingredients then we need to manually confirm 
// that it is actually different by deep comparison.

useEffect(() => {
  if (is(<similar_object>, ingredients) {
    return;
  }
  setIngredients(<similar_object>);
}, [ingredients]);

lokomotif
sumber
1

Cara terbaik adalah membandingkan nilai sebelumnya dengan nilai saat ini dengan menggunakan usePrevious () dan _.isEqual () dari Lodash . Impor isEqual dan useRef . Bandingkan nilai Anda sebelumnya dengan nilai saat ini di dalam useEffect () . Jika mereka sama, jangan perbarui lagi. usePrevious (value) adalah hook kustom yang membuat ref dengan useRef () .

Di bawah ini adalah potongan kode saya. Saya menghadapi masalah loop tak terbatas dengan memperbarui data menggunakan firebase hook

import React, { useState, useEffect, useRef } from 'react'
import 'firebase/database'
import { Redirect } from 'react-router-dom'
import { isEqual } from 'lodash'
import {
  useUserStatistics
} from '../../hooks/firebase-hooks'

export function TMDPage({ match, history, location }) {
  const usePrevious = value => {
    const ref = useRef()
    useEffect(() => {
      ref.current = value
    })
    return ref.current
  }
  const userId = match.params ? match.params.id : ''
  const teamId = location.state ? location.state.teamId : ''
  const [userStatistics] = useUserStatistics(userId, teamId)
  const previousUserStatistics = usePrevious(userStatistics)

  useEffect(() => {
      if (
        !isEqual(userStatistics, previousUserStatistics)
      ) {
        
        doSomething()
      }
     
  })

Program HS
sumber
1
Saya kira orang-orang tidak menyukai ini karena Anda menyarankan untuk menggunakan perpustakaan pihak ketiga. Namun, ide dasarnya bagus.
jperl
1

Jika Anda DO perlu membandingkan objek dan ketika diperbarui di sini adalah deepComparekait untuk perbandingan. Jawaban yang diterima tentunya tidak membahas hal itu. Memiliki []array cocok jika Anda membutuhkan efek untuk berjalan hanya sekali saat dipasang.

Selain itu, jawaban lain yang dipilih hanya menangani pemeriksaan untuk tipe primitif dengan melakukan obj.valueatau sesuatu yang serupa dengan pertama kali sampai ke tingkat di mana ia tidak bersarang. Ini mungkin bukan kasus terbaik untuk objek bersarang dalam.

Jadi inilah salah satu yang akan bekerja dalam semua kasus.

import { DependencyList } from "react";

const useDeepCompare = (
    value: DependencyList | undefined
): DependencyList | undefined => {
    const ref = useRef<DependencyList | undefined>();
    if (!isEqual(ref.current, value)) {
        ref.current = value;
    }
    return ref.current;
};

Anda bisa menggunakan yang sama di useEffecthook

React.useEffect(() => {
        setState(state);
    }, useDeepCompare([state]));
HalfWebDev
sumber
0

Anda juga dapat merusak objek dalam larik ketergantungan, yang berarti status hanya akan diperbarui ketika bagian tertentu dari objek diperbarui.

Demi contoh ini, katakanlah bahan-bahannya mengandung wortel, kita dapat meneruskannya ke dependensi, dan hanya jika wortel diubah, statusnya akan diperbarui.

Anda kemudian dapat mengambil ini lebih jauh dan hanya memperbarui jumlah wortel pada titik-titik tertentu, sehingga mengontrol kapan negara akan memperbarui dan menghindari putaran tak terbatas.

useEffect(() => {
  setIngredients({});
}, [ingredients.carrots]);

Contoh ketika sesuatu seperti ini dapat digunakan adalah ketika pengguna masuk ke situs web. Saat mereka masuk, kami dapat merusak objek pengguna untuk mengekstrak cookie dan peran izin mereka, dan memperbarui status aplikasi yang sesuai.

to240
sumber
0

Kasus saya istimewa saat menghadapi loop tanpa batas, senario seperti ini:

Saya memiliki Object, katakanlah objX yang berasal dari props dan saya merusaknya dengan props seperti:

const { something: { somePropery } } = ObjX

dan saya menggunakan someProperysebagai ketergantungan pada useEffectsuka saya :


useEffect(() => {
  // ...
}, [somePropery])

dan itu menyebabkan saya loop tak terbatas, saya mencoba menangani ini dengan melewatkan keseluruhan somethingsebagai ketergantungan dan itu bekerja dengan baik.

a_m_dev
sumber