React useEffect结合Redux触发无限循环问题及解决方案咨询
问题场景
我在Redux store中存储了一个对象数组,通过以下方式获取:
const storeExpenses = useSelector(({ expenses }: RootState) => expenses.items);
由于需要在不修改store数据的前提下进一步过滤,我将这些expense对象保存到组件的本地state中:
const [expenses, setExpensesInState] = useState<Expense[]>(storeExpenses);
当store中的expenses在其他地方更新时,我希望同步刷新本地state,于是编写了如下代码:
useEffect(() => { setExpensesInState(storeExpenses); }, [storeExpenses]);
但这导致了useEffect陷入无限循环。我的猜想是:调用setExpensesInState会触发组件重渲染,重渲染时重新获取storeExpenses,进而再次触发useEffect,如此循环。请问该猜想是否正确?如何解决此问题以实现需求?
猜想分析:方向正确,但核心原因是引用变化
你的循环逻辑描述是对的,但触发循环的根本原因是useSelector每次返回的是一个新的数组引用。
Redux的reducer遵循不可变更新原则,每次更新expenses.items时,都会返回一个新的数组(比如[...oldItems, newItem])。这意味着组件每次重渲染时,storeExpenses都会是一个全新的数组对象——哪怕数组里的内容完全没变化,它的引用也和上一次不一样。
而useEffect的依赖项比较是浅比较,只要引用变了,就会认为依赖更新,进而触发useEffect。调用setExpensesInState又会触发组件重渲染,重渲染时useSelector又返回新引用,如此形成无限循环。
解决方案:优先避免本地state同步,必要时稳定引用
方案1:直接计算过滤结果,无需本地state
这是最推荐的方案——既然你的需求是基于store数据做过滤,完全不需要把数据复制到本地state。直接在组件中结合过滤条件实时计算即可,用useMemo缓存计算结果避免重复执行:
import { useSelector } from 'react-redux'; import { useState, useMemo } from 'react'; const YourComponent = () => { // 从store获取原始数据 const storeExpenses = useSelector(({ expenses }: RootState) => expenses.items); // 过滤条件存在本地state const [filterKeyword, setFilterKeyword] = useState(''); // 缓存过滤后的结果,只有当依赖变化时才重新计算 const filteredExpenses = useMemo(() => { return storeExpenses.filter(expense => expense.title.includes(filterKeyword) // 这里替换成你的过滤逻辑 ); }, [storeExpenses, filterKeyword]); // 直接使用filteredExpenses渲染即可 return ( // ... 你的组件内容 ); };
这种方式既省去了store和本地state同步的麻烦,又保证了数据的一致性,性能也更优。
方案2:若必须用本地state,稳定storeExpenses的引用
如果确实需要在本地state中保存数据(比如要做临时编辑、不同步回store的操作),可以通过以下方式避免引用频繁变化:
方法A:用shallowEqual优化useSelector的比较逻辑
Redux提供了shallowEqual工具,让useSelector在对比数组时,只比较数组的元素(浅比较),而不是直接对比引用。只要数组的元素和长度没变化,就返回同一个引用:
import { useSelector, shallowEqual } from 'react-redux'; import { useState, useEffect } from 'react'; const YourComponent = () => { // 添加shallowEqual作为第二个参数,稳定返回引用 const storeExpenses = useSelector( ({ expenses }: RootState) => expenses.items, shallowEqual ); const [expenses, setExpensesInState] = useState<Expense[]>(storeExpenses); useEffect(() => { setExpensesInState(storeExpenses); }, [storeExpenses]); // 现在只有数组内容变化时,才会触发effect // ... 组件其他逻辑 };
方法B:使用深度比较的Effect(不推荐,性能较差)
如果不想修改useSelector,可以使用基于深度比较的useDeepCompareEffect(可以自己实现,或者用第三方库如react-use),这样只有当数组的实际内容变化时,才会触发effect:
import { useSelector } from 'react-redux'; import { useState } from 'react'; import { useDeepCompareEffect } from 'react-use'; const YourComponent = () => { const storeExpenses = useSelector(({ expenses }: RootState) => expenses.items); const [expenses, setExpensesInState] = useState<Expense[]>(storeExpenses); useDeepCompareEffect(() => { setExpensesInState(storeExpenses); }, [storeExpenses]); // ... 组件其他逻辑 };
注意:深度比较会遍历数组的每一个元素,性能开销比shallowEqual大,尽量优先用方案1或方法A。
内容的提问来源于stack exchange,提问作者phoebus

