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作为普通变量,每次组件渲染都会重新初始化,无法在轮询过程中保持状态,也会导致逻辑异常。
解决方案
用
useRef保存最新的函数引用:
将updateStatus函数存入useRef,确保定时器始终能调用到最新的函数实例,避免闭包捕获旧函数。用
useCallback稳定函数引用:
用useCallback包裹updateStatus,确保仅当依赖项变化时才更新函数引用,配合useRef实现函数的动态更新。用
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
相关产品推荐
相关产品推荐

