useEffectの無限ループ


113

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

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

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

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

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

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

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


Tobias、値が変更されたら、どのようなユースケースで成分の値を変更する必要がありますか?
ベン・カープ

@トビアス、あなたは私の答えを読むべきです。正解として受け入れていただけると思います。
HalfWebDev

回答:


127

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

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

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


1
実際には、渡す必要のある空のオブジェクトではなく、空の配列です。
GifCo19年

19
これで問題が解決しない、あなたはフックで使用される依存関係を渡す必要があります
helado

1
アンマウント時に、指定した場合、エフェクトはクリーンアップ機能を実行することに注意してください。実際の効果はアンマウントでは実行されません。 reactjs.org/docs/hooks-effect.html#example-using-hooks-1
トニー

2
setIngredietsが依存関係である場合に空の配列を使用することは、DanAbramovが言ったようにアンチパターンです。componentDidMount()メソッドのようなuseEffetctを処理しないでください
p7adams

1
:上記の人々が正しくこの問題を解決する方法についてのリンクやリソースを提供していなかったので、それは私の無限ループの問題固定するので、私は、このアウトをチェックするために対向duckduckgoersを示唆stackoverflow.com/questions/56657907/...
C.リブを

77

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

で実行するには再レンダリングすべてのコンポーネント/親あなたが使用する必要があります。

  useEffect(() => {

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

コンポーネントのマウント後に何かを1回だけ実行するには(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の変更で何かを一度実行するには:

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


@endavidは、使用している小道具であるかに依存します
ZiiMakc

1
もちろん、コンポーネントスコープの値を使用しない場合は、省略して問題ありません。しかし、純粋に設計/アーキテクチャの観点からは、これは良い習慣ではありません。小道具を使用する必要がある場合は、関数全体をエフェクト内に移動する必要があり、最終的にuseEffectメソッドを使用する可能性があるためです。法外な量のコード行。
egdavid

18

同じ問題が1回発生し、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]);

4
別のアプローチは、方法は、参照が保持され、依存関係の値は同じように評価されること、useMemo有するような値を作成することである
helado

あなたは、関数の値またはuseCallback()のためuseMemo()を使用することができます@helado
Juanmaメネンデス

そのランダムな値が依存関係の配列に渡されていますか?もちろん、これは無限ループを防ぎますが、これは推奨されますか?0依存関係の配列にも渡すことができますか?
thinkvantagedu

11

ドキュメント(https://reactjs.org/docs/hooks-effect.htmluseEffectに記載されているように、フックは、レンダリングのたびにコードを実行する場合に使用することを目的としています。ドキュメントから:

useEffectはすべてのレンダリングの後に実行されますか?はい!

これをカスタマイズしたい場合は、同じページの後半に表示される手順(https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects)に従うことができます。基本的に、このuseEffectメソッドは2番目の引数を受け入れます。この引数は、Reactが調べて、エフェクトを再度トリガーする必要があるかどうかを判断します。

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

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


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

コンポーネントコードでこれに遭遇した場合、ES6のデフォルト値の代わりにdefaultPropsを使用すると、ループが修正される可能性があります

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

MyComponent.defaultProps = {
  values = {}
}

ありがとう!これは私には自明ではなく、まさに私が直面していた問題でした。
stuckj

1
あなたは私の日を救った!ありがとう兄貴。
ハンヴァンファム

7

useEffectの最後に空の配列を含める場合:

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

それは一度実行されます。

配列にパラメータも含める場合:

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

テキストパラメータが変更されるたびに実行されます。


フックの最後に空の配列を配置した場合、なぜ1回だけ実行されるのでしょうか。
カレン

useEffectの最後にある空の配列は、たとえばuseEffect内でsetStateが必要になる可能性がある状況で、無限ループを停止するための開発者による意図的な実装です。そうしないと、useEffect->状態の更新-> useEffect->無限ループになります。
to2 4020

空の配列は、eslinterからの警告を引き起こします。
hmcclungiii

6

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

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

私の場合(私は配列をフェッチしていました!)、マウント時にデータをフェッチし、次に変更時にのみデータをフェッチし、ループに入りませんでした。


1
配列内のアイテムが置き換えられた場合はどうなりますか?その場合、配列の長さは同じになり、効果は実行されません。
アーミルカーン

4

あなたの無限ループは真円度によるものです

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

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

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

setIngrediantstimeToChangeIngrediants変更されたときに実行されます。

  1. 変更された後、どのユースケースが成分の変更を正当化するのかわかりません。ただし、その場合は、Object.values(ingrediants)useEffectの2番目の引数として渡します。
useEffect(() => {
  setIngredients({});
}, Object.values(ingrediants));

2

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

彼らは、古いデータを使用している可能性を表現し、これを認識しようとしていると思います。arrayこれらの値のいずれかが変更された場合に効果が実行されることがわかっている限り、2番目の引数に送信する値のタイプは関係ありません。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()からLodashisEqualuseRefをインポートします。以前の値をuseEffect()内の現在の値と比較します。それらが同じである場合、他に何も更新しません。usePrevious(値)が作成したカスタムフックですREFuseRefを()

以下は私のコードの抜粋です。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()
      }
     
  })


1
あなたがサードパーティのライブラリを使用することを提案したので、人々はこれに反対票を投じたと思います。ただし、基本的な考え方は優れています。
jperl

1

オブジェクトを比較する必要があり、それが更新された場合、ここdeepCompareに比較のためのフックがあります。受け入れられた答えは確かにそれに対処していません。[]マウント時にエフェクトを1回だけ実行する必要がある場合は、配列を使用するのが適しています。

また、他の投票された回答は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]);

このようなものを使用できる場合の例は、ユーザーがWebサイトにログインする場合です。ユーザーがログインすると、ユーザーオブジェクトを分解して、Cookieと権限の役割を抽出し、それに応じてアプリの状態を更新できます。


0

私のケースは無限ループに遭遇することに特別でした、シナリオは次のようでした:

私はオブジェクトを持っていました。たとえば、小道具からのobjXで、次のような小道具でそれを破壊していました。

const { something: { somePropery } } = ObjX

そして私はsomePropery私のuseEffectようなものへの依存関係としてを使用しました:


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

そしてそれは私に無限ループを引き起こしました、私は全体somethingを依存関係として渡すことによってこれを処理しようとしました、そしてそれは正しく働きました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.