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

React&TypeScript自定义Hook轮询状态更新时重置为初始状态的问题

轮询Hook中状态无法更新的问题与解决

场景

编写自定义Hook用于获取数据,启动setInterval每2秒轮询状态更新,所有数据处理完成后清除定时器停止轮询。

问题

传入setInterval的函数始终只能获取初始状态(传入的formData),即便状态已更新,疑似函数引用过时导致闭包捕获旧状态。

代码

Hook实现

export type Invoice = {id: number, status: string};

export async function executeRequest<T>(callback: () => T, afterRequest: (response: {error: string | null, data: T}) => void) {
  const response = callback();
  afterRequest({error: null, data: response});
}

export function useInvoiceProcessing(formData: Invoice[]): [result: Invoice[], executeRequest: () => Promise<void>] {

  const timer: MutableRefObject<NodeJS.Timer | undefined> = useRef();

  const [result, setResult] = useState<Invoice[]>(formData);

  // Mock API返回数据
  const firstRequestResult: Invoice[] = [
    {id: 1, status: "Success"},
    {id: 2, status: "Pending"}, // 需要轮询更新
    {id: 3, status: "Failed"}
  ];
  const firstPoll: Invoice = {id: 2, status: "Pending"};
  const secondPoll: Invoice = {id: 2, status: "Success"};

  // 用户点击触发的初始请求函数
  async function handleFirstRequest() {
    await executeRequest(() => firstRequestResult, response => {
      if (!response.error) {
        setResult(response.data)
        if (response.data.some(invoice => invoice.status === "Pending")) {
          // 启动2秒轮询
          timer.current = setInterval(() => updateStatus(), 2000);
        }
      } else {
        // 错误处理
      }
    })
  }

  let isFirstPoll = true; // 模拟首次轮询的标记
  async function updateStatus() {
    // 此处result始终是初始的formData,而非首次请求后的更新值
    console.log(result); 
    const newResult = [...result];
    let index = 0;
    for (const invoice of newResult) {
      if (invoice.status === "Pending") {
        await executeRequest(() => isFirstPoll ? firstPoll : secondPoll, response => {
          if (!response.error) {
            newResult[index] = response.data;
          } else {
            // 错误处理
          }
        });
      }
      index++;
    }
  
    setResult(newResult);
    isFirstPoll = false;
    
    const areInvoicesPending = newResult.some(invoice => invoice.status === "Pending");
    if (!areInvoicesPending) {
      console.log("Manual clear")
      clearInterval(timer.current);
    }
  };

  useEffect(() => {
    return () => {
      console.log("Unmount clear")
      clearInterval(timer.current);
    }
  }, [])

  return [result, handleFirstRequest];
}

使用示例

const [result, executeRequest] = useInvoiceProcessing([
    { id: 1, status: "NotUploaded" },
    { id: 2, status: "NotUploaded" },
    { id: 3, status: "NotUploaded" }
  ]);

  async function handleRequest() {
    console.log("Start request");
    executeRequest();
  }

  return (
    <div className="App">
      <button onClick={handleRequest}>Execute Request</button>
      <p>
        {result.map((invoice) => (
          <Fragment key={invoice.id}>
            {invoice.status}
            <br />
          </Fragment>
        ))}
      </p>
    </div>
  );
}

问题原因与解决方案

原因

这是典型的闭包陷阱:当setInterval启动时,它捕获的是当前渲染周期的updateStatus函数,该函数引用的是当时的result状态(初始的formData)。后续组件因状态更新重新渲染时,会生成新的updateStatus函数,但定时器依然调用旧的函数实例,导致始终读取旧状态值。

此外,isFirstPoll作为普通变量,每次组件渲染都会重新初始化,无法在轮询过程中保持状态,也会导致逻辑异常。

解决方案

  1. 用useRef保存最新的函数引用:
    将updateStatus函数存入useRef,确保定时器始终能调用到最新的函数实例,避免闭包捕获旧函数。

  2. 用useCallback稳定函数引用:
    用useCallback包裹updateStatus,确保仅当依赖项变化时才更新函数引用,配合useRef实现函数的动态更新。

  3. 用useRef保存状态和可变变量:
    将result和isFirstPoll存入useRef,直接从ref中读取最新值,避免闭包捕获旧状态。

修改后的关键代码

export function useInvoiceProcessing(formData: Invoice[]): [result: Invoice[], executeRequest: () => Promise<void>] {
  const timer = useRef<NodeJS.Timer | undefined>();
  const [result, setResult] = useState<Invoice[]>(formData);
  const resultRef = useRef(result);
  const isFirstPoll = useRef(true); // 用ref保存可变状态

  // 同步result到ref
  useEffect(() => {
    resultRef.current = result;
  }, [result]);

  // Mock API返回数据
  const firstRequestResult: Invoice[] = [
    {id: 1, status: "Success"},
    {id: 2, status: "Pending"},
    {id: 3, status: "Failed"}
  ];
  const firstPoll: Invoice = {id: 2, status: "Pending"};
  const secondPoll: Invoice = {id: 2, status: "Success"};

  // 用useCallback包裹updateStatus,确保依赖变化时更新
  const updateStatus = useCallback(async () => {
    const currentResult = resultRef.current;
    const newResult = [...currentResult];
    let index = 0;
    for (const invoice of newResult) {
      if (invoice.status === "Pending") {
        await executeRequest(() => isFirstPoll.current ? firstPoll : secondPoll, response => {
          if (!response.error) {
            newResult[index] = response.data;
          }
        });
      }
      index++;
    }

    setResult(newResult);
    isFirstPoll.current = false;
    
    const areInvoicesPending = newResult.some(invoice => invoice.status === "Pending");
    if (!areInvoicesPending) {
      console.log("Manual clear")
      clearInterval(timer.current);
    }
  }, []);

  // 保存updateStatus的最新引用
  const updateStatusRef = useRef(updateStatus);
  useEffect(() => {
    updateStatusRef.current = updateStatus;
  }, [updateStatus]);

  async function handleFirstRequest() {
    await executeRequest(() => firstRequestResult, response => {
      if (!response.error) {
        setResult(response.data)
        if (response.data.some(invoice => invoice.status === "Pending")) {
          // 调用ref中的最新函数
          timer.current = setInterval(() => updateStatusRef.current(), 2000);
        }
      }
    })
  }

  useEffect(() => {
    return () => {
      console.log("Unmount clear")
      clearInterval(timer.current);
    }
  }, [])

  return [result, handleFirstRequest];
}

内容的提问来源于stack exchange,提问作者Sean A.S. Mengis

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 12:20:32