You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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 });

核心逻辑说明

  1. 用useRef保存isRunning(是否有函数在执行)、latestHandler(最新的handler)、pendingHandler(等待执行的handler),确保所有函数都能获取到最新状态
  2. 当前函数执行时,若输入变化,将最新handler标记为待执行,等当前函数执行完成后再处理
  3. 加入wait参数控制输入稳定时长,避免短时间内多次变化导致频繁执行
  4. 用useState维护当前执行的Promise,供外部获取执行状态或结果

内容的提问来源于stack exchange,提问作者whiterook6

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 19:40:33