You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 06:18:24