Next.js中setState回调内Server Action被调用两次的解决方法
问题原因
在Next.js的StrictMode开发环境中,React会重复执行useState的更新器函数(即setNumLemons的回调),以此验证该函数是否为纯函数(无副作用)。你把后端请求serverActionUpdateLemons放在了这个更新器函数里,导致它被调用两次。核心问题在于:useState更新器函数必须是纯函数,不能包含后端请求这类副作用操作。
解决方案
方案1:分离状态更新与副作用(推荐)
将状态计算(纯逻辑)和后端请求(副作用)分开,符合React设计原则,同时避免重复调用。
const [numLemons, setNumLemons] = useState(initial_array); function incrementHandler(user_id) { // 纯函数逻辑:计算新状态值 const updatedLemons = numLemons.map(item => item.id === user_id ? { ...item, lemons_count: item.lemons_count + 1 } : item ); // 更新组件状态 setNumLemons(updatedLemons); // 调用后端接口(仅执行一次) const newCount = updatedLemons.find(item => item.id === user_id).lemons_count; serverActionUpdateLemons(user_id, newCount); }
说明:事件处理函数incrementHandler在StrictMode下不会被重复执行,因此后端请求只会触发一次。状态计算逻辑保持纯函数,完全符合React的要求。
方案2:用useRef标记避免重复执行(临时方案)
如果因业务限制必须在更新器函数中调用后端接口,可以用useRef跟踪执行状态,跳过StrictMode的第二次调用:
const [numLemons, setNumLemons] = useState(initial_array); const isProcessing = useRef(false); function incrementHandler(user_id) { setNumLemons(prev => { // 第二次调用时直接返回原状态,不执行副作用 if (isProcessing.current) { isProcessing.current = false; return prev; } const tempArray = [...prev]; const changeIndex = tempArray.findIndex(e => e.id === user_id); const newCount = tempArray[changeIndex].lemons_count + 1; tempArray[changeIndex] = { ...tempArray[changeIndex], lemons_count: newCount }; serverActionUpdateLemons(user_id, newCount); isProcessing.current = true; return tempArray; }); }
说明:StrictMode下更新器函数会连续调用两次,useRef的current值在两次调用间保持不变,第一次调用时执行请求并标记为已处理,第二次调用时直接返回原状态。注意这种方式会破坏更新器函数的纯函数特性,仅作为临时 workaround 使用。
方案3:关闭StrictMode(不推荐)
如果只是开发环境临时测试,可以关闭StrictMode,但不建议长期使用——StrictMode能帮助提前发现潜在的副作用问题,对代码质量有重要作用。
在Next.js 13+的app/layout.js中,移除<StrictMode>包裹:
// app/layout.js export default function RootLayout({ children }) { return ( <html lang="zh-CN"> <body>{children}</body> </html> ); }
说明:生产环境下StrictMode默认关闭,线上不会出现重复调用问题,但开发环境会失去StrictMode的校验能力。
内容的提问来源于stack exchange,提问作者Dibly

