渲染层报错复盘(Web + Electron)
1. 背景
在完成 Zustand Store 迁移后,web 与 desktop(electron renderer) 先后出现 React 渲染错误,主要表现为:
Invalid hook callCannot read properties of null (reading 'useRef' / 'useState')Incompatible React versions
这些错误会导致页面无法渲染,属于高优先级阻塞问题。
2. 问题现象
2.1 Web 端
关键报错:
Invalid hook call. Hooks can only be called inside of the body of a function component.- 触发点:
<BrowserRouter>(main.tsx:9) - 连带报错:
Cannot read properties of null (reading 'useRef')
2.2 Web 端二次报错(修复过程中的中间态)
关键报错:
Incompatible React versionsreact: 19.1.0react-dom: 19.2.5
说明渲染时拿到的 react 与 react-dom 不是同一版本。
2.3 Electron 渲染进程
关键报错:
Invalid hook callCannot read properties of null (reading 'useState')- 触发组件:
Versions.tsx
3. 根因分析
根因 A:Monorepo 下 React 实例混用
在 workspace 场景下,不同 app 的 react、react-dom 解析路径可能不同,导致:
- 业务代码使用了一份
react - 渲染器(
react-dom/ router)使用了另一份react
从而触发 Invalid hook call。
根因 B:一次错误修复带来的版本错配
为修复 Web 端 Invalid hook call,曾尝试在 Vite 中把 react alias 到 workspace 根。随后发现根目录实际版本为:
root react = 19.1.0root react-dom = 19.2.5
这会直接触发 Incompatible React versions。
根因 C:Electron 渲染端依赖归类导致运行时解析不稳定
apps/desktop 初始将 react、react-dom 放在 devDependencies,配合 monorepo hoist 后,渲染进程更容易解析到非预期位置的包。
4. 排查路径(实际执行)
定位异常入口:
apps/web/src/main.tsx的<BrowserRouter>apps/desktop/src/renderer/src/components/Versions.tsx
检查版本与依赖树:
pnpm --filter @aitodos/web why reactpnpm --filter @aitodos/store why reactpnpm --filter desktop why react
检查运行时解析路径(关键):
require.resolve('react')require.resolve('react-dom')- 分别在
apps/web、apps/admin、apps/desktop目录执行
检查本地安装版本:
node_modules/react/package.jsonnode_modules/react-dom/package.json
5. 修复动作(按时间顺序)
5.1 Web/Admin 首轮修复
文件:
apps/web/vite.config.tsapps/admin/vite.config.ts
动作:
- 增加
resolve.dedupe: ['react', 'react-dom']
作用:
- 降低同一应用加载多份 React 的风险。
5.2 修复过程中的错误动作(已回滚)
动作:
- 在 Web/Admin Vite 里把
react/react-domalias 到 workspace 根
结果:
- 命中了
root react=19.1.0与react-dom=19.2.5的版本错配 - 出现
Incompatible React versions
处理:
- 回滚该 alias,仅保留
dedupe
5.3 Electron Renderer 修复
文件:
apps/desktop/electron.vite.config.tsapps/desktop/package.json
动作 1(解析一致化):
- 在
renderer.resolve中保留dedupe: ['react', 'react-dom'] - 显式增加:
react: resolve('node_modules/react')
动作 2(依赖归类修正):
- 将
react、react-dom从devDependencies移到dependencies
目的:
- 保证 Electron 渲染进程运行时拿到同一套 React 运行时依赖。
5.4 依赖重装与重启
执行:
pnpm install --filter desktop
pnpm --filter desktop dev
结果:
- 安装成功(exit code 0)
- Electron renderer dev server 启动成功
6. 最终状态
- Web 端渲染恢复正常
- Electron 渲染进程可启动并进入页面
Invalid hook call链路已完成定向修复
7. 可复用修复模板
当再次出现 Invalid hook call 时,建议按以下顺序:
先查“版本一致性”
react与react-dom必须完全同版本
再查“解析路径一致性”
- 比较
require.resolve('react')与require.resolve('react-dom')
- 比较
在 bundler 中启用去重
resolve.dedupe: ['react', 'react-dom']
对 Electron/运行时 app,确保 React 是运行时依赖
- 放在
dependencies而非仅devDependencies
- 放在
每次改完配置必须重启进程
- 停掉旧的 dev 进程
- 重新启动并强刷页面
8. 备注
apps/desktop的pnpm.onlyBuiltDependencies配置警告属于配置位置提示,不是本次渲染错误根因。- 若后续仍偶发异常,优先加运行时日志打印 React 解析路径和版本再定位。
9. 补充:Electron 复用 Web 后样式丢失与 CSP 报错
9.1 问题现象 A:页面“有结构、无样式”
现象:
- Electron 页面能渲染文本和结构,但 Tailwind 视觉样式几乎全部失效。
- 页面看起来像纯 HTML 默认样式。
关联报错(阶段性):
[plugin:vite:import-analysis] Failed to resolve import "@web/index.css"
根因:
main.tsx直接从@web/index.css引入时,CSS alias 在 Electron + Vite 场景下解析不稳定。apps/desktop原模板样式文件未显式扫描apps/web/src,导致 Tailwind 未生成 Web 页面实际使用的 utility class。
修复:
apps/desktop/src/renderer/src/main.tsx- 使用 desktop 本地样式入口:
import './assets/main.css'
- 使用 desktop 本地样式入口:
apps/desktop/src/renderer/src/assets/main.css- 保留
@import "tailwindcss"; - 增加扫描源:
@source "../../../../../web/src/**/*.{ts,tsx}";@source "../**/*.{ts,tsx}";
- 移除 Electron 模板页遗留样式,保留通用基础规则(
html/body/#root)。
- 保留
验证:
pnpm --filter desktop build通过。- 构建后的 renderer CSS 体积明显增大(说明 Tailwind utility 已成功生成)。
9.2 问题现象 B:CSP 拒绝 unsafe-eval
关键报错:
Uncaught EvalError: Evaluating a string as JavaScript violates CSP ... 'unsafe-eval'- 触发点:
packages/shared/src/storage.ts
根因:
- 为了条件加载 Taro/RN 存储实现,代码中使用了
new Function(...)进行动态导入; - Electron 渲染进程默认 CSP 不允许
unsafe-eval,因此直接报错。
修复:
packages/shared/src/storage.ts- 将
new Function(...)改为 CSP 安全写法:import(/* @vite-ignore */ modulePath)
- 将
验证:
pnpm --filter @aitodos/shared type-check通过。pnpm --filter desktop typecheck通过。- Electron 运行时不再出现
unsafe-eval相关异常。
9.3 结论
- 复用 Web 到 Electron 时,样式链路要以 desktop 为入口统一管理;
- Tailwind v4 需要在 desktop 的 CSS 入口显式声明
@source扫描 Web 源码; - 渲染层共享代码必须避免
eval/new Function,否则容易被 CSP 拦截。