React状态提升与修改器传递时useEffect触发无限循环问题
解决React组件useEffect无限循环问题(因匿名onChange函数导致)
问题场景
子组件DamageInput通过useEffect监听内部状态变化,调用父组件传入的onChange回调同步状态:
export default function DamageInput({ data, onChange }){ const [qty, setQty] = useState(data?.qty || null); const [selectedSize, setSelectedSize] = useState(data?.size || null); const [selectedSuppl, setSelectedSuppl] = useState(data?.suppl || []); useEffect(() => { if(!selectedSize) onChange(null); else { const damageObject = { qty: qty, size: selectedSize, suppl: selectedSuppl } onChange(damageObject); } }, [selectedSize, qty, selectedSuppl, onChange]); ... }
父组件通过匿名函数传递onChange:
<DamageInput onChange={(damageObject) => handleDamageChange('bonnet', damageObject)} data={data.damage.bonnet} />
父组件的状态更新函数:
const handleDamageChange = (panel, damageObject) => { if(!damageObject){ if(data.damage[panel]){ setData(prevState => { const damageCopy = {...prevState.damage}; delete damageCopy[panel]; return {...prevState, damage: damageCopy}; }); } } else setData(prevState => ({...prevState, damage: {...prevState.damage, [panel]: damageObject}})); };
问题现象:子组件useEffect触发无限循环。
原因分析:父组件传递的onChange是匿名函数,每次父组件渲染都会生成新的函数实例。子组件useEffect依赖onChange,导致每次父组件更新后,子组件useEffect都会重新执行,调用onChange又触发父组件状态更新,形成循环。
可行解决办法
方案1:用useCallback缓存回调函数
在父组件中,用useCallback缓存状态更新函数或绑定参数后的回调,确保函数引用稳定:
import { useCallback } from 'react'; // 缓存核心处理函数,改用函数式更新避免依赖外部data const handleDamageChange = useCallback((panel, damageObject) => { setData(prevState => { const newDamage = {...prevState.damage}; if (!damageObject) { delete newDamage[panel]; } else { newDamage[panel] = damageObject; } return {...prevState, damage: newDamage}; }); }, []); // 无外部依赖,函数引用永久稳定 // 缓存绑定bonnet的回调 const bonnetOnChange = useCallback((damageObject) => { handleDamageChange('bonnet', damageObject); }, [handleDamageChange]); // 父组件调用时传递缓存后的函数 <DamageInput onChange={bonnetOnChange} data={data.damage.bonnet} />
方案2:子组件用useRef保存最新的onChange
子组件中通过useRef存储最新的onChange,useEffect不再依赖onChange,而是通过ref.current调用:
import { useRef, useEffect, useState } from 'react'; export default function DamageInput({ data, onChange }){ const onChangeRef = useRef(onChange); // 每次props更新时同步最新的onChange到ref useEffect(() => { onChangeRef.current = onChange; }, [onChange]); const [qty, setQty] = useState(data?.qty || null); const [selectedSize, setSelectedSize] = useState(data?.size || null); const [selectedSuppl, setSelectedSuppl] = useState(data?.suppl || []); useEffect(() => { // 从ref获取最新的onChange函数 const currentOnChange = onChangeRef.current; if (!selectedSize) { currentOnChange(null); } else { const damageObject = { qty, size: selectedSize, suppl: selectedSuppl }; currentOnChange(damageObject); } }, [selectedSize, qty, selectedSuppl]); // 仅依赖内部状态,不再依赖onChange ... }
方案3:直接传递参数给子组件,避免匿名函数
修改子组件,新增panel props,让子组件直接调用父组件的无参数绑定函数:
// 子组件新增panel props export default function DamageInput({ data, onChange, panel }){ // ...内部状态不变 useEffect(() => { if (!selectedSize) { onChange(panel, null); } else { const damageObject = { qty, size: selectedSize, suppl: selectedSuppl }; onChange(panel, damageObject); } }, [selectedSize, qty, selectedSuppl, onChange, panel]); ... } // 父组件调用时,直接传递handleDamageChange和panel参数 <DamageInput onChange={handleDamageChange} panel="bonnet" data={data.damage.bonnet} />
同时父组件的handleDamageChange用useCallback缓存,确保引用稳定:
const handleDamageChange = useCallback((panel, damageObject) => { setData(prevState => { const newDamage = {...prevState.damage}; if (!damageObject) { delete newDamage[panel]; } else { newDamage[panel] = damageObject; } return {...prevState, damage: newDamage}; }); }, []);
内容的提问来源于stack exchange,提问作者Daniel Kormos
相关产品推荐
相关产品推荐

