为何useState钩子会被调用两次?附复现测试用例
React useState更新回调重复执行问题
问题现象
提交请求后,明明只调用了一次updateCurrentConversation,但setConversations的回调函数却执行了两次,控制台输出两条相同的更新日志,最终导致对话中出现重复的响应内容。
复现场景
- 用户输入查询内容并点击发送按钮
- 创建新的对话对象并添加到对话列表
- 调用
invokeServerAPI处理请求,传入响应回调函数实时展示结果 - 回调触发后更新当前对话,但
setConversations的更新逻辑被执行两次
控制台输出
page.tsx:25 Text response received: Response for query:sample query 1 page.tsx:31 Invoke updateCurrentConversation with response: Response for query:sample query 1 page.tsx:31 Invoke updateCurrentConversation with response: Response for query:sample query 1
问题代码
"use client" import React, { useState, useRef } from 'react'; export default function MyApp() { const [input, setInput] = useState(''); const [conversations, setConversations] = useState([]); const messagesEndRef = useRef(null); const handleSubmit = async (event) => { event.preventDefault(); const startTime = new Date() const newConversation = { query: input, startTime, responses: [] } setConversations((prevConversations) => [...prevConversations, newConversation]); try { await invokeServerAPI(input, textHandler) } finally { setInput(''); } function textHandler(text) { console.log("Text response received:", text) updateCurrentConversation(text) } function updateCurrentConversation(data) { setConversations((prevConversations) => { console.log('Invoke updateCurrentConversation with response: ', data) const index = prevConversations.findIndex(conversation => conversation.startTime === startTime); const _newConversation = { ...prevConversations[index] }; _newConversation.responses.push({ data }) return [ ...prevConversations.slice(0, index), _newConversation, ...prevConversations.slice(index + 1) ]; }); } }; return ( <div className="flex flex-col h-screen"> <div className="overflow-y-scroll flex-grow p-2"> {conversations.map((conversation, index) => ( <div key={index} className="mb-5"> <p>{conversation.startTime.toLocaleString()} <span className="font-bold">{conversation.query})</span>:</p> {conversation.responses .map((response, respIndex) => { return ( <div key={index * 1000 + respIndex} className="mb-2"> <pre className="mt-2 bg-gray-100 p-2 rounded border border-gray-300 whitespace-pre-wrap">{response.data}</pre> </div> ); }) } </div> ))} <div ref={messagesEndRef} /> </div> <div className="p-2 bg-gray-800 text-white"> <form onSubmit={handleSubmit}> <div className="flex justify-start space-x-4"> <div className="flex flex-col" style={{ marginRight: '1rem', marginLeft: '0' }}> <label className="text-black" style={{ color: 'black', display: 'block', width: 'auto' }}>Query:</label> <input type="text" value={input} onChange={(e) => setInput(e.target.value)} className="p-2 bg-gray-300 text-black rounded-md" style={{ fontSize: '18px', color: 'black', height: '40px', width: '500px' }} placeholder="Enter your query here" /> </div> <div className="flex flex-col items-start"> <label style={{ color: 'black', display: 'block', width: 'auto' }}> </label> <button type="submit" className="bg-green-500 text-white rounded-md hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50" style={{ paddingLeft: '1rem', paddingRight: '1rem', height: '40px', marginLeft: '0' }}>Send</button> </div> </div> </form> </div> </div > ); } export function invokeServerAPI(theQuery, theHandler) { return new Promise((resolve, reject) => { // 模拟单次响应 theHandler('Response for query:' + theQuery) resolve(null); }) }
问题原因
1. React严格模式(Strict Mode)
这是最核心的原因。在开发环境下,React严格模式会重复调用状态更新函数、副作用函数,目的是检测代码中是否存在非幂等的副作用逻辑。你的代码标注了"use client",在Next.js等现代框架中,客户端组件默认启用严格模式,因此setConversations的回调会被执行两次。
2. 对话标识不可靠
你用startTime作为查找对话的唯一标识,但Date的精度有限(部分环境下仅到毫秒),如果用户快速连续提交请求,可能会出现相同的startTime,导致错误更新其他对话。
解决方案
方案1:开发环境忽略该现象
严格模式的重复执行仅存在于开发环境,生产环境会自动禁用该行为。如果只是开发时的日志干扰,无需额外处理。
方案2:关闭严格模式
若想彻底消除开发环境的重复执行,可以在框架配置中关闭严格模式。以Next.js为例,修改next.config.js:
/** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: false, } module.exports = nextConfig
方案3:确保状态更新逻辑幂等
即使更新函数被执行多次,也不会产生错误结果。比如在添加响应前检查是否已存在:
function updateCurrentConversation(data) { setConversations((prevConversations) => { const index = prevConversations.findIndex(conversation => conversation.id === conversationId); const targetConversation = prevConversations[index]; // 检查是否已添加过该响应 const hasResponse = targetConversation.responses.some(r => r.data === data); if (hasResponse) return prevConversations; const _newConversation = { ...targetConversation }; _newConversation.responses.push({ data }); return [ ...prevConversations.slice(0, index), _newConversation, ...prevConversations.slice(index + 1) ]; }); }
方案4:使用唯一ID标识对话
替换startTime为唯一ID,避免标识冲突:
// 创建对话时生成唯一ID const newConversation = { id: crypto.randomUUID(), query: input, startTime: new Date(), responses: [] }; // 保存对话ID到闭包中 const conversationId = newConversation.id; // 更新时用ID查找 const index = prevConversations.findIndex(conversation => conversation.id === conversationId);
内容的提问来源于stack exchange,提问作者Ya.
相关产品推荐
相关产品推荐

