React中定时器调用set方法时useState状态不更新问题排查
问题:React中useState变量无法正确更新,setHash看似未生效
我有一个WebSocket服务器,每15秒发送一个包含哈希值的对象。客户端收到哈希后,需要与当前存储的哈希对比,若不同则调用API获取新数据。WebSocket功能正常,能正确发送哈希,当服务器数据更新时我会收到不同的哈希值,但存储当前哈希的hash变量无法正确更新。
为排查问题,我禁用了组件中的WebSocket监听,改用setInterval模拟更新,代码如下:
import { useCallback, useEffect, useState } from "react"; import { useAuth, useSocket } from "../utils/hooks"; const Admin = () => { const [ questionLists, setQuestionLists ] = useState<QuestionListModel[]>([]); const { user } = useAuth(); const { socket } = useSocket(); const [ hash, setHash ] = useState<Hash>({questionList: ""}); const fetchHash = useCallback(async () => { setHash({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"}); }, []); const fetchQuestionLists = useCallback(async () => { console.log("fetching new question lists"); const response: ApiResponse | boolean = await getQuestionLists(user?.token); if (typeof response !== "boolean" && response.data) { setQuestionLists(response.data); } }, [hash]); useEffect(() => { fetchHash(); fetchQuestionLists(); }, []); const update = useCallback((newHash: Hash) => { console.log("called update"); let shouldUpdate = false; let originalHash = { ...hash }; let updatedHash = { ...newHash }; console.log("new: ", newHash); console.log("stored: ", originalHash); if (hash.questionList !== newHash.questionList) { console.log("was not equal"); updatedHash = { ...updatedHash, questionList: newHash.questionList} shouldUpdate = true; } if (shouldUpdate) { console.log("trying to set new hash: ", updatedHash); setHash(updatedHash); fetchQuestionLists(); } }, [hash]); /*useEffect(() => { socket?.on('aHash', (fetchedHash) => update(fetchedHash)); }, []);*/ useEffect(() => { setInterval(() => { update({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"}); }, 15000) }, []); return ( <> ... Things here later ... </> ); }; export default Admin;
初始渲染后等待两个定时器周期,控制台输出如下:
fetching new question lists called update new: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'} stored: {questionList: ''} was not equal trying to set new hash: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'} fetching new question lists called update new: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'} stored: {questionList: ''} was not equal trying to set new hash: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'} fetching new question lists
可以看到stored的哈希值为空,似乎setHash(updatedHash)未生效,请问这是什么原因?
原因分析与解决方案
核心原因:闭包导致的过期状态(stale state)
update函数通过useCallback包裹,依赖数组为[hash],但启动setInterval的useEffect未将update加入依赖,导致定时器调用的update始终是组件首次渲染时的版本,该版本捕获的hash是初始值{questionList: ""},后续setHash更新状态后,旧的update函数内的hash不会同步更新。- 注释掉的WebSocket监听
useEffect同样未依赖update,也会导致监听回调调用的是旧版本update,捕获初始hash值。
具体解决方案
1. 用函数式更新获取最新状态,修复update闭包问题
不要直接依赖外部hash,而是通过setHash的函数式更新参数获取最新状态,同时调整useCallback依赖:
const update = useCallback((newHash: Hash) => { console.log("called update"); setHash(prevHash => { let shouldUpdate = false; let updatedHash = { ...prevHash }; console.log("new: ", newHash); console.log("stored: ", prevHash); if (prevHash.questionList !== newHash.questionList) { console.log("was not equal"); updatedHash.questionList = newHash.questionList; shouldUpdate = true; } if (shouldUpdate) { console.log("trying to set new hash: ", updatedHash); fetchQuestionLists(); return updatedHash; } return prevHash; }); }, [fetchQuestionLists]);
2. 修复定时器与WebSocket监听的依赖,添加清理逻辑
启动setInterval的useEffect需加入update依赖,同时清除定时器避免内存泄漏:
useEffect(() => { const intervalId = setInterval(() => { update({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"}); }, 15000); return () => clearInterval(intervalId); }, [update]);
恢复WebSocket监听时,同样加入update依赖,并在组件卸载时移除监听:
useEffect(() => { if (!socket) return; const handleHash = (fetchedHash) => update(fetchedHash); socket.on('aHash', handleHash); return () => socket.off('aHash', handleHash); }, [socket, update]);
3. 优化fetchQuestionLists的依赖
fetchQuestionLists无需依赖hash,因为它会在哈希更新后主动调用,只需保留user?.token作为依赖:
const fetchQuestionLists = useCallback(async () => { console.log("fetching new question lists"); if (!user?.token) return; const response: ApiResponse | boolean = await getQuestionLists(user.token); if (typeof response !== "boolean" && response.data) { setQuestionLists(response.data); } }, [user?.token]);
4. 简化初始化逻辑
初始useEffect可直接调用update完成初始化,无需单独fetchHash:
useEffect(() => { update({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"}); }, [update]);
内容的提问来源于stack exchange,提问作者BluePrint
相关产品推荐
相关产品推荐

