不使用useEffectEvent,如何避免useForm自定义Hook中的无限循环?
问题场景
我正在开发一个简易表单Hook,需求是表单维护初始值与当前值,支持将当前值重置为初始值,同时允许重置时修改初始值。最初的实现如下:
const useForm = <T>(initialInitialValue: T) => { // 用state存储初始值,因为reset可以修改它 const [initialValue, setInitialValue] = useState(initialInitialValue); const [value, setValue] = useState(initialInitialValue); const reset = useCallback((newInitialValue?: T) => { if (newInitialValue !== undefined) { setInitialValue(newInitialValue); } setValue(newInitialValue ?? initialValue); }, [initialValue]); return { reset, setValue, value }; };
但调用这个Hook时很容易触发无限循环,比如在useEffect中等待异步数据(如react-query返回的结果)后调用reset:
// 等待myInitialValue就绪(比如从react-query的useQuery获取) useEffect(() => { if (myInitialValue === undefined) { return; } reset(myInitialValue); }, [myInitialValue, reset]);
问题根源
问题出在reset函数的依赖上:reset依赖initialValue状态,当调用reset(newInitialValue)时,会修改initialValue,导致reset被重新创建。而useEffect的依赖数组包含reset,因此reset更新后会再次触发useEffect,进而再次调用reset,形成无限循环。
虽然可以用useRef配合useEffect做单次调用的取巧处理,但这种方案需要每个使用者都知晓并手动实现,属于抽象泄漏,非常脆弱。未来的useEffectEvent虽然能优化这类场景,但目前尚未稳定可用,且仍将避免循环的责任推给了调用方。
推荐实现方案
方案一:用useRef持久化初始值,让reset保持稳定
核心思路是用useRef存储初始值,而非useState,这样reset函数不需要依赖任何状态,始终保持同一个引用,从根源上避免依赖变化导致的循环。
const useForm = <T>(initialInitialValue: T) => { // 用useRef持久化初始值,不受重新渲染影响 const initialValueRef = useRef(initialInitialValue); const [value, setValue] = useState(initialInitialValue); const reset = useCallback((newInitialValue?: T) => { if (newInitialValue !== undefined) { // 更新ref中的初始值 initialValueRef.current = newInitialValue; } // 重置当前值,优先用新传入的初始值,否则用ref中保存的初始值 setValue(newInitialValue ?? initialValueRef.current); }, []); // 无依赖,reset始终是同一个函数 return { reset, setValue, value }; };
方案二:保留initialValue状态,用函数式更新避免依赖
如果必须用useState存储初始值,可以通过函数式更新获取最新的初始值,同时移除reset对initialValue的依赖:
const useForm = <T>(initialInitialValue: T) => { const [initialValue, setInitialValue] = useState(initialInitialValue); const [value, setValue] = useState(initialInitialValue); const reset = useCallback((newInitialValue?: T) => { if (newInitialValue !== undefined) { // 同时更新初始值和当前值 setInitialValue(newInitialValue); setValue(newInitialValue); } else { // 用函数式更新获取最新的initialValue,避免依赖 setValue(() => initialValue); } }, []); // 依赖为空,reset保持稳定 return { reset, setValue, value }; };
效果验证
优化后的useForm中,reset函数始终是同一个引用,调用方无需额外处理,直接在useEffect中使用即可避免无限循环:
useEffect(() => { if (myInitialValue === undefined) return; reset(myInitialValue); }, [myInitialValue, reset]);
此时只有myInitialValue变化时才会触发useEffect,reset的调用不会导致自身重新创建,彻底解决循环问题。
内容的提问来源于stack exchange,提问作者tibbe

