React StrictMode双重渲染导致globalID重复自增的解决方案
这不是StrictMode的异常表现,恰恰是StrictMode帮你提前暴露了代码里的不规范写法:
- 你把id计数器
globalID定义在组件外部的全局作用域,属于不受React状态管控的可变共享变量 - 你在
setTodos的状态更新回调里直接执行globalID++修改外部变量,这属于状态更新过程中的副作用,违反了React对状态更新函数必须是纯函数的要求:相同输入必须返回相同输出,不能修改外部作用域的变量。
React 18之后StrictMode在开发环境下会主动重复执行渲染逻辑、状态更新函数、初始effect挂载卸载流程,目的就是提前检测这类不纯的代码。这类逻辑本身在生产环境遇到并发渲染、状态重试等场景时,一样会出现id重复跳号的问题,StrictMode只是把本来可能在线上爆发的bug提前挪到了开发阶段让你修复,完全不需要关闭StrictMode。
只需要把id生成逻辑改成符合React纯函数要求的写法即可,两种常用实现方案按需选择:
方案1:完全不依赖外部变量,基于已有状态生成新id
去掉组件外的全局globalID变量,直接在状态更新回调里基于已有的todo列表计算新id,保证更新函数是纯函数,无论执行多少次都返回一致的结果:
function createTodo(event) { event.preventDefault(); setTodos(oldTodos => { // 取现有todo的最大id+1作为新id,避免删除条目后出现id重复 const newId = oldTodos.length === 0 ? 0 : Math.max(...oldTodos.map(item => item.id)) + 1; return [...oldTodos, { todo: task, id: newId }] }); setTask(''); }
这种写法下,哪怕StrictMode重复执行多少次setTodos的更新逻辑,只要传入的oldTodos不变,生成的新id就不会变,完全不会出现跳号问题。
方案2:用useRef存储计数器,把自增逻辑移到状态更新外
如果你习惯用递增计数器的逻辑,不要把变量放在组件全局作用域,用useRef把计数器存在组件实例内部,同时把自增逻辑从setTodos的回调里移到事件处理函数外层——用户交互触发的事件处理函数不会被StrictMode重复执行,在这里修改变量是安全的:
import React, { useState, useRef } from 'react'; import './App.css'; function App() { const [task, setTask] = useState(''); const [todos, setTodos] = useState([]); // 用ref存储id计数器,组件生命周期内持久化,不会随重渲染重置 const idCounterRef = useRef(0); function createTodo(event) { event.preventDefault(); // 事件处理函数不会被StrictMode重复执行,在这里自增是安全的 const newItemId = idCounterRef.current++; setTodos(oldTodos => [...oldTodos, { todo: task, id: newItemId }]); setTask(''); } function deleteItem(itemID) { setTodos(oldTodos => oldTodos.filter(item => item.id !== itemID)) } return ( <div> <form onSubmit={createTodo}> <h1>Best To Do App Ever</h1> <input type="text" value={task} onChange={(event) => { setTask(event.target.value); }} /> <button>Create Todo</button> </form> <ul> {todos.map((item) => { return ( <div key={item.id}> <li> {item.todo} ({item.id}) </li> <button onClick={() => deleteItem(item.id)}> Delete </button> </div> ); })} </ul> </div> ); } export default App;
注意:不要把
idCounterRef.current++写在setTodos的箭头函数内部,状态更新回调是会被StrictMode重复执行的,写在内部依然会出现重复自增的问题。
StrictMode的重复执行逻辑仅在开发环境生效,生产环境构建后不会触发这类重复检测。保留StrictMode能帮你提前规避大量React并发特性相关的潜在bug,遇到重复执行导致的异常时,优先修正代码里的不纯逻辑,不要直接关闭StrictMode。
内容的提问来源于stack exchange,提问作者fisherbone

