web-dev-qa-db-ja.com

useEffectの無限ループ

React 16.7-alphaの新しいフックシステムをいじくり回しており、処理中の状態がオブジェクトまたは配列の場合、useEffectで無限ループに陥ります。

まず、useStateを使用して、次のような空のオブジェクトで開始します。

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

次に、useEffectで、setObjを使用して空のオブジェクトに再度設定します。 2番目の引数として、オブジェクトのcontentが変更されていなければ更新されないことを期待して、[obj]を渡します。しかし、それは更新し続けます。内容に関係なく、これらは常に異なるオブジェクトであり、Reactが変化し続けると考えているからでしょうか。

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

配列についても同じことが言えますが、プリミティブとしては期待どおりにループにはまりません。

これらの新しいフックを使用して、コンテンツが変更されたかどうかを確認するときにオブジェクトと配列をどのように処理する必要がありますか?

36
Tobias Haugen

UseEffectの2番目の引数として空の配列を渡すと、マウント時とアンマウント時にのみ実行され、無限ループが停止します。

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

これは https://www.robinwieruch.de/react-hooks/ のReactフックに関するブログ投稿で明確にされました。

37
Tobias Haugen

同じ問題があった。なぜドキュメントでこれに言及していないのか分かりません。 Tobias Haugenの回答に少し追加したいだけです。

every component/parent rerenderで実行するには、使用する必要があります:

  useEffect(() => {

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

何かを実行するには1回のみコンポーネントのマウント後(一度レンダリングされます):

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

何かを実行するにはコンポーネントマウントおよびdata/data2で1回変更:

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

ドキュメント( https://reactjs.org/docs/hooks-effect.html )で述べたように、useEffectフックは使用することを意図しています各renderの後にコードを実行する場合。ドキュメントから:

UseEffectはレンダリングのたびに実行されますか?はい!

これをカスタマイズする場合は、同じページに後で表示される指示に従ってください( https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-効果 )。基本的に、useEffectメソッドは、2番目の引数を受け入れ、そのReactは、エフェクトが再度トリガーされるかどうか。

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

任意のオブジェクトを2番目の引数として渡すことができます。 このオブジェクトが変更されないままの場合、エフェクトは最初のマウント後にのみトリガーされます。オブジェクトが変更されると、エフェクトが再びトリガーされます。

私も同じ問題に一度遭遇し、2番目の引数[]でプリミティブ値を渡すようにして修正しました。

オブジェクトを渡すと、Reactはオブジェクトへの参照のみを保存し、参照が変更されたときにエフェクトを実行します。これは通常、1回ごとに行われます(方法はわかりません)。

解決策は、オブジェクトに値を渡すことです。あなたが試すことができます、

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]);
5
Dinesh Pandiyan

これがうまくいくかどうかはわかりませんが、次のように.lengthを追加してみてください:

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

私の場合(配列を取得していました!)、マウント時にデータを取得し、変更時にのみデータを取得し、ループしませんでした。

2
Luka M

あなたの無限ループは円形性による

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

setIngredients({});ingredientsの値を変更し(毎回新しい参照を返します)、setIngredients({})を実行します。これを解決するには、次のいずれかのアプローチを使用できます。

  1. UseEffectに別の2番目の引数を渡します
const timeToChangeIngrediants = .....
useEffect(() => {
  setIngredients({});
}, [timeToChangeIngrediants ]);

setIngrediantsが変更されると、timeToChangeIngrediantsが実行されます。

  1. 変更された変更内容を正当化するユースケースがわからない。ただし、その場合は、useEffectの2番目の引数としてObject.values(ingrediants)を渡します。
useEffect(() => {
  setIngredients({});
}, Object.values(ingrediants));
1
Ben Carp

この最適化を使用する場合は、時間とともに変化し、エフェクトで使用されるコンポーネントスコープ(propsやstateなど)のすべての値が配列に含まれていることを確認してください。

古いデータを使用している可能性を表明し、これを認識しようとしていると思います。これらの値のいずれかが変更された場合、エフェクトが実行されることがわかっている限り、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
locomotif

最良の方法は、sePrevious()および_。isEqual() from Lodashを使用して、以前の値を現在の値と比較することです。インポートisEqualおよびseRefseEffect()内の以前の値と現在の値を比較します。それらが同じ場合、他に何も更新しません。 sePrevious(value)は、seRef()refを作成するカスタムフックです。

以下は私のコードのスニペットです。 firebaseフックを使用してデータを更新すると無限ループの問題に直面していました

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()
      }
     
  })
0
H S Progr