การวนซ้ำที่ไม่มีที่สิ้นสุดในการใช้งาน


115

ฉันเล่นกับระบบ hook ใหม่ใน React 16.7-alpha และติดอยู่ในวงวนที่ไม่มีที่สิ้นสุดใน useEffect เมื่อสถานะที่ฉันจัดการคือวัตถุหรืออาร์เรย์

ขั้นแรกฉันใช้ useState และเริ่มต้นด้วยวัตถุว่างเช่นนี้:

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

จากนั้นใน useEffect ฉันใช้ setObj เพื่อตั้งค่าเป็นวัตถุว่างอีกครั้ง ในฐานะอาร์กิวเมนต์ที่สองฉันกำลังส่ง [obj] โดยหวังว่าจะไม่อัปเดตหากเนื้อหาของวัตถุไม่เปลี่ยนแปลง แต่จะอัปเดตอยู่เสมอ ฉันเดาว่าเพราะไม่ว่าเนื้อหาจะเป็นอย่างไรสิ่งเหล่านี้เป็นวัตถุที่แตกต่างกันเสมอทำให้ React คิดว่ามันเปลี่ยนไปเรื่อย ๆ ?

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

เช่นเดียวกับอาร์เรย์ แต่เป็นแบบดั้งเดิมมันจะไม่ติดอยู่ในลูปอย่างที่คาดไว้

เมื่อใช้ hooks ใหม่เหล่านี้ฉันจะจัดการกับวัตถุและอาร์เรย์ได้อย่างไรเมื่อตรวจสอบสภาพอากาศว่าเนื้อหามีการเปลี่ยนแปลงหรือไม่


โทเบียสกรณีการใช้งานใดที่ต้องเปลี่ยนมูลค่าของส่วนผสมเมื่อค่าของมันเปลี่ยนไปแล้ว?
Ben Carp

@ โทเบียสคุณควรอ่านคำตอบของฉัน ฉันแน่ใจว่าคุณจะยอมรับว่าเป็นคำตอบที่ถูกต้อง
HalfWebDev

คำตอบ:


128

การส่งอาร์เรย์ว่างเป็นอาร์กิวเมนต์ที่สองไปยัง useEffect ทำให้รันบนเมานต์และยกเลิกการต่อเชื่อมเท่านั้นดังนั้นจึงหยุดลูปที่ไม่มีที่สิ้นสุด

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

สิ่งนี้ได้ชี้แจงให้ฉันทราบในบล็อกโพสต์เกี่ยวกับ React hooks ที่https://www.robinwieruch.de/react-hooks/


1
จริงๆแล้วมันเป็นอาร์เรย์ว่างเปล่าไม่ใช่วัตถุว่างที่คุณควรส่งผ่าน
GifCo

20
วิธีนี้ไม่สามารถแก้ปัญหาได้คุณต้องส่งผ่านการอ้างอิงที่ใช้โดย hook
helado

1
โปรดทราบว่าเมื่อยกเลิกการต่อเชื่อมเอฟเฟกต์จะเรียกใช้ฟังก์ชันล้างข้อมูลหากคุณได้ระบุไว้ เอฟเฟกต์จริงไม่ทำงานเมื่อยกเลิกการต่อเชื่อม reactjs.org/docs/hooks-effect.html#example-using-hooks-1
tony

2
การใช้อาร์เรย์ว่างเมื่อ setIngrediets เป็นการพึ่งพาคือการต่อต้านรูปแบบดังที่ Dan Abramov กล่าว อย่าปฏิบัติกับ useEffetct เหมือนวิธี componentDidMount ()
p7adams

1
เนื่องจากคนข้างบนไม่ได้ให้ลิงค์หรือแหล่งข้อมูลเกี่ยวกับวิธีการแก้ปัญหานี้อย่างถูกต้องฉันขอแนะนำให้ผู้ที่กำลังจะมาตรวจสอบปัญหานี้เนื่องจากมันได้แก้ไขปัญหาการวนซ้ำที่ไม่สิ้นสุดของฉัน: stackoverflow.com/questions/56657907/…
C. Rib

78

มีปัญหาเดียวกัน ฉันไม่รู้ว่าทำไมพวกเขาไม่พูดถึงสิ่งนี้ในเอกสาร เพียงต้องการเพิ่มคำตอบของ Tobias Haugen เล็กน้อย

ในการรันในทุกองค์ประกอบ / เรนเดอร์พาเรนต์คุณต้องใช้:

  useEffect(() => {

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

ในการรันสิ่งใด ๆเพียงครั้งเดียวหลังจากการเมานต์ส่วนประกอบ (จะแสดงผลครั้งเดียว) คุณต้องใช้:

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

ในการเรียกใช้อะไรก็ได้ในครั้งเดียวบนการเชื่อมต่อคอมโพเนนต์และการเปลี่ยนแปลงข้อมูล / 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] )

ฉันใช้มันเกือบตลอดเวลา:

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>  
}

2
ตามการตอบสนอง faq, มันไม่ปลอดภัยที่จะฟังก์ชั่นงดจากรายการอ้างอิง
egdavid

@endavid ขึ้นอยู่กับสิ่งที่คุณใช้
ZiiMakc

1
แน่นอนว่าหากคุณไม่ได้ใช้ค่าใด ๆจากขอบเขตส่วนประกอบคุณสามารถละเว้นได้อย่างปลอดภัย แต่จากจุดออกแบบ / สถาปัตยกรรมของ vue อย่างหมดจดนี่ไม่ใช่แนวปฏิบัติที่ดีเนื่องจากคุณต้องย้ายฟังก์ชั่นทั้งหมดของคุณภายในเอฟเฟกต์หากคุณจำเป็นต้องใช้อุปกรณ์ประกอบฉากและคุณสามารถจบลงด้วยวิธี useEffect ที่จะใช้ จำนวนบรรทัดโค้ดที่อุกอาจ
egdavid

18

[]ฉันวิ่งเข้าไปในปัญหาเดียวกันเมื่อเกินไปและฉันคงได้โดยการทำให้แน่ใจว่าผมส่งผ่านค่าดั้งเดิมในอาร์กิวเมนต์ที่สอง

หากคุณส่งผ่านวัตถุ React จะจัดเก็บเฉพาะการอ้างอิงไปยังวัตถุและเรียกใช้เอฟเฟกต์เมื่อการอ้างอิงเปลี่ยนไปซึ่งโดยปกติจะเป็นเวลาเดี่ยวทุกครั้ง (ตอนนี้ฉันไม่รู้ว่าเป็นอย่างไร)

วิธีแก้ปัญหาคือการส่งผ่านค่าในวัตถุ คุณสามารถลอง,

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

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

หรือ

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

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

4
อีกวิธีหนึ่งคือการสร้างค่าดังกล่าวด้วย useMemo วิธีนั้นจะเก็บข้อมูลอ้างอิงและค่าของการอ้างอิงจะได้รับการประเมินเหมือนกัน
helado

@helado คุณสามารถใช้ useMemo () สำหรับค่าหรือ useCallback () สำหรับฟังก์ชัน
Juanma Menendez

ค่าสุ่มนั้นถูกส่งผ่านไปยังอาร์เรย์การอ้างอิงหรือไม่? แน่นอนว่านี่เป็นการป้องกันการวนซ้ำที่ไม่มีที่สิ้นสุด แต่สิ่งนี้ได้รับการสนับสนุนหรือไม่? ฉันสามารถส่งผ่าน0ไปยังอาร์เรย์การอ้างอิงได้เช่นกัน?
thinkvantagedu

11

ตามที่กล่าวในเอกสาร ( https://reactjs.org/docs/hooks-effect.html ) ที่useEffectตะขอจะหมายถึงการถูกนำมาใช้เมื่อคุณต้องการโค้ดบางส่วนที่จะดำเนินการทุกครั้งหลังการแสดงผล จากเอกสาร:

useEffect ทำงานทุกครั้งหลังการแสดงผลหรือไม่ ใช่

หากคุณต้องการปรับแต่งสิ่งนี้คุณสามารถทำตามคำแนะนำที่ปรากฏในภายหลังในหน้าเดียวกัน ( https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects ) โดยทั่วไปuseEffectเมธอดยอมรับอาร์กิวเมนต์ที่สองซึ่ง React จะตรวจสอบเพื่อพิจารณาว่าเอฟเฟกต์ต้องถูกทริกเกอร์อีกครั้งหรือไม่

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

คุณสามารถส่งผ่านวัตถุใดก็ได้เป็นอาร์กิวเมนต์ที่สอง หากวัตถุนี้ยังคงไม่เปลี่ยนแปลงเอฟเฟกต์ของคุณจะถูกเรียกใช้หลังจากการเมานต์ครั้งแรกเท่านั้น หากวัตถุเปลี่ยนแปลงเอฟเฟกต์จะถูกเรียกใช้อีกครั้ง


11

หากคุณกำลังสร้างเบ็ดแบบกำหนดเองบางครั้งคุณอาจทำให้เกิดการวนซ้ำแบบไม่สิ้นสุดโดยมีค่าเริ่มต้นดังนี้

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

การแก้ไขคือการใช้ออบเจ็กต์เดียวกันแทนการสร้างใหม่ในทุกการเรียกใช้ฟังก์ชัน:

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

หากคุณพบสิ่งนี้ในโค้ดส่วนประกอบของคุณลูปอาจได้รับการแก้ไขหากคุณใช้ defaultProps แทนค่าเริ่มต้น ES6

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

MyComponent.defaultProps = {
  values = {}
}

ขอบคุณ! สิ่งนี้ไม่ชัดเจนสำหรับฉันและเป็นปัญหาที่ฉันพบ
เจ

1
คุณช่วยวันของฉัน! ขอบคุณครับ
Han Van Pham

7

ฉันไม่แน่ใจว่าจะได้ผลสำหรับคุณหรือไม่ แต่คุณสามารถลองเพิ่ม. length ดังนี้:

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

ในกรณีของฉัน (ฉันกำลังดึงอาร์เรย์!) มันดึงข้อมูลบนเมานต์จากนั้นอีกครั้งเมื่อมีการเปลี่ยนแปลงเท่านั้นและมันไม่ได้วนซ้ำ


1
จะเกิดอะไรขึ้นถ้ารายการถูกแทนที่ในอาร์เรย์? ในกรณีนี้ความยาวของอาร์เรย์จะเท่ากันและเอฟเฟกต์จะไม่ทำงาน!
Aamir Khan

7

หากคุณรวมอาร์เรย์ว่างไว้เมื่อสิ้นสุดการใช้งานผล :

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

มันจะทำงานครั้งเดียว

หากคุณรวมพารามิเตอร์ไว้ในอาร์เรย์ด้วย:

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

มันจะทำงานเมื่อใดก็ตามที่พารามิเตอร์ข้อความเปลี่ยนแปลง


ทำไมมันถึงทำงานเพียงครั้งเดียวถ้าเราใส่อาร์เรย์ว่างที่ส่วนท้ายของตะขอ?
กะเหรี่ยง

อาร์เรย์ว่างที่ส่วนท้ายของ useEffect เป็นการใช้งานโดยมีจุดประสงค์โดยนักพัฒนาเพื่อหยุดการวนซ้ำแบบไม่สิ้นสุดในสถานการณ์ที่คุณอาจจำเป็นต้อง setState ภายใน useEffect มิฉะนั้นจะนำไปสู่ ​​useEffect -> state update -> useEffect -> infinite loop
ถึง 240

อาร์เรย์ว่างทำให้เกิดคำเตือนจาก eslinter
hmcclungiii

4

การวนซ้ำที่ไม่มีที่สิ้นสุดของคุณเกิดจากความเป็นวงกลม

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

setIngredients({});จะเปลี่ยนค่าของingredients(จะกลับอ้างอิงใหม่ในแต่ละครั้ง) setIngredients({})ซึ่งจะทำงาน ในการแก้ปัญหานี้คุณสามารถใช้วิธีใดวิธีหนึ่ง:

  1. ส่งอาร์กิวเมนต์ที่สองที่แตกต่างกันเพื่อ useEffect
const timeToChangeIngrediants = .....
useEffect(() => {
  setIngredients({});
}, [timeToChangeIngrediants ]);

setIngrediantsจะทำงานเมื่อtimeToChangeIngrediantsมีการเปลี่ยนแปลง

  1. ฉันไม่แน่ใจว่ากรณีการใช้งานแบบใดที่แสดงถึงการเปลี่ยนแปลงของส่วนผสมเมื่อมีการเปลี่ยนแปลง แต่ถ้าเป็นกรณีนี้คุณจะส่งต่อObject.values(ingrediants)เป็นอาร์กิวเมนต์ที่สองเพื่อ useEffect
useEffect(() => {
  setIngredients({});
}, Object.values(ingrediants));

2

หากคุณใช้การเพิ่มประสิทธิภาพนี้ตรวจสอบให้แน่ใจว่าอาร์เรย์มีค่าทั้งหมดจากขอบเขตส่วนประกอบ (เช่นอุปกรณ์ประกอบฉากและสถานะ) ที่เปลี่ยนแปลงตลอดเวลาและใช้โดยเอฟเฟกต์

ฉันเชื่อว่าพวกเขาพยายามแสดงความเป็นไปได้ที่อาจมีคนใช้ข้อมูลเก่าและต้องระวังเรื่องนี้ ไม่สำคัญว่าประเภทของค่าที่เราส่งในarrayอาร์กิวเมนต์ที่สองตราบใดที่เรารู้ว่าหากมีการเปลี่ยนแปลงค่าใด ๆ จะทำให้เกิดผล หากเราใช้ingredientsเป็นส่วนหนึ่งของการคำนวณภายในเอฟเฟกต์เราควรรวมไว้ในไฟล์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]);


1

วิธีที่ดีที่สุดคือการเปรียบเทียบค่าก่อนหน้านี้ที่มีมูลค่าปัจจุบันโดยใช้usePrevious ()และ_.isEqual ()จากLodash นำเข้าIsEqualและuseRef เปรียบเทียบค่าก่อนหน้านี้ของคุณมีมูลค่าปัจจุบันภายในuseEffect () หากเหมือนกันอย่าทำการอัปเดตอย่างอื่น usePrevious (ค่า)คือ hook ที่กำหนดเองซึ่งสร้างการอ้างอิงด้วยuseRef ()()

ด้านล่างนี้คือข้อมูลโค้ดของฉัน ฉันประสบปัญหาของการวนซ้ำที่ไม่มีที่สิ้นสุดในการอัปเดตข้อมูลโดยใช้ 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()
      }
     
  })


1
ฉันเดาว่าผู้คนลดคะแนนสิ่งนี้เนื่องจากคุณแนะนำให้ใช้ไลบรารีของบุคคลที่สาม อย่างไรก็ตามแนวคิดพื้นฐานเป็นสิ่งที่ดี
jperl

1

ในกรณีที่คุณจำเป็นต้องเปรียบเทียบวัตถุและเมื่อมีการอัปเดตนี่คือdeepCompareขอเกี่ยวเพื่อเปรียบเทียบ คำตอบที่ได้รับการยอมรับไม่ได้กล่าวถึงเรื่องนั้นอย่างแน่นอน มี[]อาร์เรย์จะเหมาะสมหากคุณต้องการให้เอฟเฟกต์ทำงานเพียงครั้งเดียวเมื่อเมาท์

นอกจากนี้คำตอบที่ได้รับการโหวตอื่น ๆ จะระบุเฉพาะการตรวจสอบประเภทดั้งเดิมโดยการทำ obj.valueหรือสิ่งที่คล้ายกันเพื่อไปยังระดับที่ไม่ได้ซ้อนกันก่อน นี่อาจไม่ใช่กรณีที่ดีที่สุดสำหรับวัตถุที่ซ้อนกันลึก

ดังนั้นนี่คือสิ่งที่ใช้ได้ในทุกกรณี

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;
};

คุณสามารถใช้useEffectตะขอเดียวกันได้

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

0

คุณยังสามารถทำลายโครงสร้างของวัตถุในอาร์เรย์การอ้างอิงซึ่งหมายความว่าสถานะจะอัปเดตเมื่อมีการอัปเดตบางส่วนของวัตถุเท่านั้น

เพื่อประโยชน์ของตัวอย่างนี้สมมติว่าส่วนผสมมีแครอทเราสามารถส่งต่อไปยังการพึ่งพาได้และหากแครอทเปลี่ยนไปสถานะจะอัปเดต

จากนั้นคุณสามารถดำเนินการต่อไปและอัปเดตจำนวนแครอทในบางจุดเท่านั้นซึ่งจะควบคุมได้ว่าเมื่อใดที่จะอัปเดตและหลีกเลี่ยงการวนซ้ำที่ไม่สิ้นสุด

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

ตัวอย่างเมื่อสามารถใช้สิ่งนี้ได้คือเมื่อผู้ใช้เข้าสู่ระบบเว็บไซต์ เมื่อพวกเขาเข้าสู่ระบบเราสามารถทำลายโครงสร้างวัตถุของผู้ใช้เพื่อแยกคุกกี้และบทบาทสิทธิ์และอัปเดตสถานะของแอปตามนั้น


0

กรณีของฉันพิเศษในการเผชิญหน้ากับลูปที่ไม่มีที่สิ้นสุด Senario เป็นเช่นนี้:

ฉันมีวัตถุสมมติว่า objX ที่มาจากอุปกรณ์ประกอบฉากและฉันทำลายมันด้วยอุปกรณ์ประกอบฉากเช่น:

const { something: { somePropery } } = ObjX

และฉันใช้someProperyเป็นที่พึ่งพากับสิ่งที่ฉันuseEffectชอบ:


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

และมันทำให้ฉันวนซ้ำไม่สิ้นสุดฉันพยายามจัดการกับสิ่งนี้โดยส่งต่อทั้งหมดsomethingเป็นการพึ่งพาและมันทำงานได้อย่างถูกต้อง

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.