一次 React 项目 lock 文件冲突修复:从 Hook 报错到 Vite 配置优化
在日常开发中,分支合并是高频操作,但稍有不慎就可能引发依赖相关的“连锁反应”。本文记录了一次 rebase main 后因 lock 文件冲突,导致 React Hook 报错的完整排查与解决过程,希望能为遇到类似问题的开发者提供参考。
一、背景:rebase main 引发的“意外”
最近在开发一个基于 React + Vite + Mobx 的项目,为了同步主分支的最新代码,我执行了 git rebase main 操作。过程中遇到了 package-lock.json 冲突,由于当时急于推进开发,我直接手动编辑了冲突文件,保留了双方的依赖配置后提交了代码。
本以为只是简单的文件合并,没想到启动项目后,浏览器控制台直接抛出了一连串报错:
报错堆栈指向 mobx-react-lite 中的 useObserver 方法,提示 useRef 无法读取 null 属性。更奇怪的是,这些代码在 rebase 前完全正常,没有任何语法或逻辑修改。
二、问题分析:锁定核心矛盾
1. 排除代码逻辑问题
首先排查业务代码:近期未修改 Hook 调用逻辑,所有 useRef、useState 等 Hooks 均符合“顶层调用”规则,且未在条件、循环或事件处理函数中调用。排除代码本身的问题后,将目光聚焦到依赖和构建配置上。
2. 定位依赖层面问题
根据 React 官方文档提示,Hook 调用异常的三大常见原因:
- 违反 Hooks 使用规则(已排除);
- React 与渲染器(如 React DOM)版本不匹配;
- 项目中存在多个 React 实例。
结合“仅 lock 文件冲突后出现问题”的场景,重点排查后两点:
- 执行
npm ls react react-dom查看依赖树,- 发现输出中,Terminal#1-14 显示面板同时存在两版 mobx-react-lite :直接依赖 4.1.0 ,通过 mobx-react@9.2.1 间接带入 4.1.1 。这会让它们各自沿着不同的依赖解析路径去找 react ,在多入口/预打包的情况下,很容易把两份 React 打到同一页面。
- 进一步验证:在打包文件中搜索package.json中的react版本号18.3.1,或者搜索react源码中的ReactCurrentDispatch。可以发现合了代码之后,构建产物两个chunk中都有react。
代码修改前的打包资源
代码修改后的打包资源
3. 追溯问题根源
lock 文件的核心作用是锁定依赖的安装路径和版本。手动合并冲突时,错误保留了不同分支的依赖配置,导致 npm install 时出现依赖嵌套安装:
- 项目和项目依赖的包都依赖了
mobx-react-lite并且版本不同。 - 打包产物中,两个chunk中各自有一个react
- 运行时,就产生了两个react实例
React Hooks 的运行依赖单一的调度器实例,当 mobx-react-lite 中的 useObserver 调用嵌套依赖的 React 实例时,会因调度器不匹配导致 Hook 调用失效,进而抛出 useRef 读取 null 的错误。
三、尝试修改:从依赖到配置的逐步排查
1. 重置依赖(首次尝试失败)
首先想到的是修复依赖树,执行以下操作:
# 清除本地依赖和缓存
rm -rf node_modules package-lock.json
npm cache clean --force
# 重新安装依赖
npm install
但重新安装后,npm ls react 仍显示存在嵌套版本。推测是 mobx-react-lite 的依赖声明中未将 React 设为 peerDependency,导致 npm 自动安装兼容版本的嵌套依赖。
2. 强制统一依赖版本(部分缓解)
通过 npm install react@18.2.0 react-dom@18.2.0 --force 强制指定 React 版本,重新安装后嵌套依赖消失。但启动项目后,仍偶尔出现 Hook 报错,排查发现是 Vite 开发环境预构建时未正确识别依赖,导致部分代码仍引用旧版本缓存。
3. 优化 Vite 配置(最终突破)
结合之前对 Vite dedupe 和 optimizeDeps 的了解,意识到需要从构建层面确保依赖的唯一性和预构建的完整性:
resolve.dedupe:强制 Vite 将所有 React 相关依赖解析为根目录版本,杜绝多实例;optimizeDeps.include:强制预构建核心依赖,避免预构建漏检导致的缓存问题。
四、解决问题:最终生效的配置方案
1. 固化 Vite 配置
修改 vite.config.js,添加依赖去重和预构建配置:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
resolve: {
// 去重 React 相关核心依赖,确保单一实例
dedupe: ['react', 'react-dom', 'mobx-react-lite'],
},
optimizeDeps: {
// 强制预构建核心依赖,避免漏检
include: ['react', 'react-dom', 'mobx-react-lite'],
// 预构建阶段再次去重,双重保障
dedupe: ['react', 'react-dom'],
},
})
2. 清理缓存并验证
执行 vite --force 强制清除预构建缓存,重新启动项目后:
- 浏览器控制台无任何 Hook 相关报错;
- 执行
npm ls react react-dom仅显示根目录单一版本; - 打印 React 实例对比结果为
true,确认多实例问题彻底解决。
五、总结与反思
这次问题的核心是“lock 文件冲突处理不当”,但背后暴露了对依赖管理和构建工具配置的认知缺口。总结几点关键经验:
- lock 文件冲突切勿手动修改:遇到 lock 文件冲突时,优先执行
git checkout -- package-lock.json回滚,再通过rm -rf node_modules && npm install重新安装,避免依赖树混乱; - 依赖声明需规范:第三方库应将 React 等核心依赖设为 peerDependency,而非直接依赖,避免嵌套安装;
- Vite 配置的“防护作用” :对于 React、Vue 等核心依赖,建议在 Vite 配置中提前设置
dedupe和optimizeDeps.include,从构建层面规避多实例和预构建问题; - 报错排查要结合官方文档:React 官方明确列出了 Hook 调用异常的三大原因,排查时应先对照文档缩小范围,避免盲目尝试。
此次排查过程虽曲折,但也加深了对依赖管理、Vite 构建原理和 React Hooks 运行机制的理解。希望这篇记录能帮助大家在遇到类似问题时少走弯路~