如何在useEffect中获取最新状态?自定义防抖Hook遇stale state问题
自定义防抖Hook的问题与需求
我正在尝试构建一个debounce(防抖)Hook,但常见的实现都不符合需求:通常这类实现会延迟执行handler,直到调用操作停止足够长的时间,比如:
useEffect(() => { timeout = setTimeout(handler, 500); return () => { if (timeout){ clearTimeout(timeout); } } }, [handler]);
这种方式有缺陷——如果目的是避免频繁调用耗时函数,它完全没考虑函数本身的执行时长。比如如果搜索接口请求耗时超过500毫秒,这种防抖逻辑根本起不到预期作用。
我想要的逻辑是:
- 运行一个异步耗时函数(handler)时,若当前没有函数在执行,直接执行并返回其Promise
- 函数执行完成后,检查输入是否发生变化,若变化则再次触发handler
期望用法:
const [input, setInput] = useState<string>(""); const debouncedPromise = useDebounce(() => asyncFunction(input), [input]);
要求输入变化时,若当前无handler在运行则立即执行;若有则等待当前执行完成后,再执行最新的handler。
我写了如下代码,但首次调用正常后,state.isWaiting始终无法更新为false,导致后续handler无法触发:
import { DependencyList, useEffect, useRef, useState } from "react"; interface IState<T> { handler?: () => Promise<T>; promise?: Promise<T>; isWaiting: boolean; } export const useDebounce = <T>(handler: () => Promise<T>, deps: DependencyList = []): Promise<T> | undefined => { const [state, setState] = useState<IState<T>>({ handler, isWaiting: false }); const stopWaiting = () => { console.log("stopWaiting"); setState(previousState => ({ ...previousState, waiting: false })); }; const run = () => { const promise = handler(); promise.finally(stopWaiting); setState({ handler, isWaiting: true, promise, }); }; useEffect(() => { console.log("\nuseEffect"); console.log(`deps: ${deps}`) console.log(`state.isWaiting: ${state.isWaiting}`); console.log(`state.handler: ${state.handler}`); console.log(`state.promise: ${state.promise}`); if (state.isWaiting){ console.log(">>> state.isWaiting") return; } if (handler === state.handler){ console.log(">>> handler === state.handler") return; } if (state.isWaiting && state.promise && state.handler !== handler){ console.log(">>> state.isWaiting && state.promise && state.handler !== handler") state.promise.finally(run); return; } if (handler !== state.handler){ console.log(">>> handler !== state.handler") run(); } console.log("end useEffect"); }, [...deps, state.isWaiting]); return state.promise; };
控制台输出显示isWaiting始终为true:
useEffect UseDebounce.ts:32 deps: T UseDebounce.ts:33 state.isWaiting: false UseDebounce.ts:34 state.handler: function () { return asyncFunction(input); } UseDebounce.ts:35 state.promise: undefined UseDebounce.ts:36 >>> handler !== state.handler UseDebounce.ts:55 asyncFunction called with T UseDebounce.tsx:9 end useEffect UseDebounce.ts:59 useEffect UseDebounce.ts:32 deps: T UseDebounce.ts:33 state.isWaiting: true UseDebounce.ts:34 state.handler: function () { return asyncFunction(input); } UseDebounce.ts:35 state.promise: [object Promise] UseDebounce.ts:36 >>> state.isWaiting UseDebounce.ts:39 asyncFunction resolved with T UseDebounce.tsx:12 stopWaiting UseDebounce.ts:16 useEffect UseDebounce.ts:32 deps: Ti UseDebounce.ts:33 state.isWaiting: true UseDebounce.ts:34 // 这里应该是false
我怀疑是遇到了stale state(过期状态)问题,请问如何解决?有没有更合适的Hook实现方式?另外如何在输入“稳定”后停止触发handler?
问题分析与解决方案
1. 直接错误:state属性名写错
你在stopWaiting里犯了一个低级错误——把isWaiting写成了waiting:
// 错误写法 setState(previousState => ({ ...previousState, waiting: false })); // 正确写法 setState(previousState => ({ ...previousState, isWaiting: false }));
这直接导致isWaiting状态永远不会被重置为false,是最核心的问题。
2. 解决Stale State问题
即使修复了上面的错误,依然会遇到stale state问题——useEffect和run/stopWaiting函数捕获的是旧的state和handler引用。改用useRef来保存最新的状态和handler,因为ref的更新是同步的,能始终获取到最新值。
3. 优化后的完整实现
下面是修复并优化后的Hook,同时加入了“输入稳定后停止触发”的逻辑(通过wait参数控制输入稳定时长):
import { DependencyList, useEffect, useRef, useState } from "react"; export const useDebounce = <T>( handler: () => Promise<T>, deps: DependencyList = [], options: { wait?: number } = {} ): Promise<T> | undefined => { const { wait = 0 } = options; // 输入稳定等待时长,设为0则不等待直接执行 const [currentPromise, setCurrentPromise] = useState<Promise<T> | undefined>(); // 用ref保存最新状态,避免stale state const stateRef = useRef({ isRunning: false, latestHandler: handler, pendingHandler: null as (() => Promise<T>) | null, timeoutId: null as NodeJS.Timeout | null }); const executeHandler = async (targetHandler: () => Promise<T>) => { // 清除可能存在的稳定等待定时器 if (stateRef.current.timeoutId) { clearTimeout(stateRef.current.timeoutId); stateRef.current.timeoutId = null; } stateRef.current.isRunning = true; const promise = targetHandler(); setCurrentPromise(promise); try { await promise; } finally { stateRef.current.isRunning = false; // 检查是否有等待执行的handler if (stateRef.current.pendingHandler) { const pending = stateRef.current.pendingHandler; stateRef.current.pendingHandler = null; if (wait > 0) { // 等待输入稳定后执行 stateRef.current.timeoutId = setTimeout(() => { // 再次检查是否有更新的pending handler,避免执行过时的 if (stateRef.current.pendingHandler === null) { executeHandler(pending); } }, wait); } else { executeHandler(pending); } } setCurrentPromise(undefined); } }; useEffect(() => { stateRef.current.latestHandler = handler; if (stateRef.current.isRunning) { // 当前有函数在执行,标记最新handler为待执行 stateRef.current.pendingHandler = handler; return; } // 没有函数在执行,直接执行最新handler executeHandler(handler); // 组件卸载时清理定时器 return () => { if (stateRef.current.timeoutId) { clearTimeout(stateRef.current.timeoutId); } }; }, [...deps]); return currentPromise; };
4. 用法说明
- 基础用法(无稳定等待,执行完立即跑最新的):
const [input, setInput] = useState<string>(""); const debouncedPromise = useDebounce(() => asyncFunction(input), [input]);
- 输入稳定后再执行(比如等待300ms无变化再执行):
const debouncedPromise = useDebounce(() => asyncFunction(input), [input], { wait: 300 });
核心逻辑说明
- 用
useRef保存isRunning(是否有函数在执行)、latestHandler(最新的handler)、pendingHandler(等待执行的handler),确保所有函数都能获取到最新状态 - 当前函数执行时,若输入变化,将最新handler标记为待执行,等当前函数执行完成后再处理
- 加入
wait参数控制输入稳定时长,避免短时间内多次变化导致频繁执行 - 用
useState维护当前执行的Promise,供外部获取执行状态或结果
内容的提问来源于stack exchange,提问作者whiterook6
相关产品推荐
相关产品推荐

