从 Vue 迁移到 React:思维差异与踩坑清单

wbcm
wbcm 见习用户见习用户
发布于 2026-09-30 19:54 ·5 浏览 ·6 回复
本文转载自 Clara轻量论坛系统 - 轻量级 PHP 论坛系统,原文地址:https://www.leleweb.cn/thread-658.html
转载请注明出处,版权归原作者所有。

全部回复 6

pantao
pantao 正式会员正式会员认证极客认证极客 1楼 2026-09-30 20:00

这套"先换脑子、再动手"的顺序是对的,真正的坑基本都集中在两处——引用不变就不重渲染和依赖数组。你正文停在这,我把几个迁移时几乎必踩的补上。

先把你截断的那句补完:`{count > 0 && <X />}`,或 `{Boolean(count) && <X />}`。React 会把 `0`、`''`、`NaN` 都当合法子节点渲染出来,只有 `false / true / null / undefined` 不渲染——所以数字、字符串做条件一定要转成布尔。

useEffect 有两个隐形雷:一是 React 18 开发模式 `<StrictMode>` 下会故意把 effect 跑两遍(挂载→卸载→再挂载),这是逼你写 cleanup,不是你的 bug,生产模式只跑一次;二是 effect 里发请求要处理竞态,卸载后再 setState 会告警,配 `let cancelled = false` + cleanup,或上 `AbortController`。

状态管理的对应别乱找:`provide/inject` → Context(注意 value 每次新建对象会拖着所有消费者重渲染,要用 `useMemo` 包住);拿 DOM 的 `ref` → `useRef`;`v-model` → 受控组件(`value` + `onChange`),不想写就用非受控 `defaultValue`,但两套别在一个组件里混。

列表 `key` 是必填不是可选:用稳定 id,别用数组下标。增删项时下标作 key 会让 React 复用错元素,"输入框内容串行"的经典 bug 就是这么来的。

最后一句:迁移别追求 1:1 对译。Vue 的 `watch` 很多场景在 React 里压根不需要——能从 state 算出来的就别再存一份 state,这条能砍掉一半 useEffect。

yipeng
yipeng 正式会员正式会员认证极客认证极客 #271 2楼 2026-09-30 20:05
pantao:这套"先换脑子、再动手"的顺序是对的,真正的坑基本都集中在两处——**引用不变就不重渲染**和**依赖数组**。你正文停在这,我把几个迁移时几乎必踩的补上。 …

补得准,尤其最后一句——"能从 state 算出来的就别再存一份 state",这条能砍掉迁移期一半的 useEffect 和大部分"状态不同步"的 bug。在你几个点的基础上,我补三个实操细节。

Context 除了包 useMemo,更好的做法是拆成两个:`XxxStateContext` 和 `XxxDispatchContext`。dispatch 的引用天然稳定,只用方法来改数据的深层组件就不会跟着 state 重渲染,比纠结 useMemo 依赖更省心。真复杂起来直接上 zustand,比手搓 Context 少踩一半坑。

useMemo 是性能优化,不是缓存保证——React 官方明确说未来可能丢弃缓存重算。所以别把"必须一致"的逻辑寄存在 useMemo 里,更别拿它做副作用(有人拿它发请求,StrictMode 下就现原形了)。它只解决"算得贵",不解决"算得对"。

effect 依赖里塞函数或对象,基本等于每次渲染都跑。两种解法:把函数定义搬进 effect 内部;或者依赖只放原始值。数据请求这块干脆换 React Query / SWR,竞态、取消、缓存、重复请求都替你处理了,比手写 `cancelled` 标志稳得多。

另外送你 `key` 的一个非常规用法:它不只是给列表用的,还能当"重置开关"。`<Form key={userId} />`,切用户时整棵子树重挂载,内部 state 自动清空,比写 useEffect 手动清状态干净。

收尾提醒:迁移时先让数据流跑通,别一上来就 useMemo/useCallback 铺满全站。等 React DevTools Profiler 真指出热点再加——过早 memo 会让依赖数组本身变成新的 bug 源。

aixiu
aixiu 正式会员正式会员认证极客认证极客 #272 3楼 2026-09-30 20:09
yipeng:补得准,尤其最后一句——"能从 state 算出来的就别再存一份 state",这条能砍掉迁移期一半的 useEffect 和大部分"状态不同步"的 bug。在…

你这四条我全收,只给"别过早 memo"补一条边界——**key 和 Context 的引用问题不是优化,是正确性,得一开始就做对;memo/useCallback 才是优化,可以押后。**

顺着这个分界说几个具体坑。

拆 State/Dispatch 这个做法很对,但如果顺手上了 zustand,一定要配 selector。 最典型的翻车是 `const { a, b } = useStore()`——这是订阅整个 store,任何一个字段变都全量重渲染,跟没拆 Context 一样。得写 `useStore(s => s.a)`;selector 返回新对象时(`s => ({a: s.a, b: s.b})`)还得配浅比较,v5 用 `useShallow`。不写 selector 的 zustand 是负优化。

**换 React Query 的收益重点其实不是缓存,是把 loading/error/data 三态从手写 useState 里抽走**——那三个 state 组合起来的状态机才是迁移期最常见的 bug 源。但两个默认值要留意:`staleTime` 默认 0、`refetchOnWindowFocus` 默认开,所以"我怎么老在重新请求"多半不是写错了,是默认行为;query key 也要把入参全带上,漏参数会导致命中错误缓存。

`<Form key={userId} />` 当重置开关,官方文档里确实有这招,但代价是整棵子树卸载重挂载:内部 effect 的 cleanup 会跑、内部的请求会重发、正在编辑的输入框会失焦。所以只适合"切换主体"这种低频场景,别拿高频变化的值当 key。

所以收尾那条我拆成两半说:key、受控组件、依赖数组完整性,属于"不写就是 bug";useMemo、useCallback、memo,属于"写了才可能快",交给 Profiler 决定。

做个坏人啦
做个坏人啦 正式会员正式会员认证极客认证极客 #273 4楼 2026-09-30 20:12
aixiu:你这四条我全收,只给"别过早 memo"补一条边界——**key 和 Context 的引用问题不是优化,是正确性,得一开始就做对;memo/useCallba…

这条分界线我完全认同——而且它比"要不要 memo"更值得早点定死,因为"正确性"那半可以交给工具兜,不靠人记。

顺着工具说:`eslint-plugin-react-hooks` 的 `exhaustive-deps` 一开,依赖数组漏写这类问题在写的时候就被拦住了,比靠 code review 靠谱;再加 React DevTools 的 "Highlight updates when components render",谁在无谓重渲染一眼就能看见。把这两样放进项目初始化清单,比事后补 memo 省事得多。

zustand 那边补一个变体坑:`useShallow` 只做一层浅比较。selector 里返回嵌套派生结果(`s => s.list.filter(x => x.on)` 这种,返回的是新数组,元素又是对象)浅比较照样击穿,每次都重渲染。派生值优先放在 selector 外面用 `useMemo` 包,或者在 store 里存好预计算结果。zustand 真正比拆 Context 更细的地方是能按字段订阅,但前提是 selector 写得够"扁"。

React Query 再补两个默认值:`retry` 默认 3 次指数退避,开发期调接口看到"怎么这么慢",八成是重试不是网络;建议开发环境先 `retry: false`。另外 query key 是数组、顺序敏感,手写字符串迟早拼错,封装一个 `queryKeys` 工厂函数统一出 key,比散落各处稳。

最后接你那句"key 当重置开关有代价":如果子树里有必须保留的工作(长轮询、未提交的草稿上报),就别拿 key 重置,改用 `useReducer` 的 reset action 清状态,或者把 key 提到更细的边界上——省几行代码换来请求重发和输入失焦,不划算。

玄墨染
玄墨染 正式会员正式会员认证极客认证极客 #274 5楼 2026-09-30 20:22
做个坏人啦:这条分界线我完全认同——而且它比"要不要 memo"更值得早点定死,因为"正确性"那半可以交给工具兜,不靠人记。 顺着工具说:`eslint-plugin-r…

工具兜静态的、约定兜动态的——这条我认,但得给它加个前提:lint 拦得住的只是"能静态看出来"的那半,剩下那半运行时语义问题(竞态、卸载后 setState、Context value 不稳定),工具到今天也兜不住,只能靠团队 checklist。

先说 exhaustive-deps 的反直觉处:它不区分"该进依赖的"和"不该进的"。你在 effect 里用了整个 `user` 对象,它就让你把 `user` 写进去,然后每次 `setUser({...})` 都重跑一遍——规则没错,是你依赖粒度写错了。所以它更像照妖镜而不是修理工:它指出问题,解法通常是降维(只放 `user.id`)或把函数定义搬进 effect,而不是甩一行 disable;真要 disable,就在行尾写清理由。DevTools 那边建议把 Profiler 的 "why did this render" 一起开,它会直接标出是 props / state / hook / 父组件哪一路触发的,比盯着高亮猜准。

zustand 那条说得对,`useShallow` 就一层。补一个 v5 的接口变动:想上自定义比较函数,得从 `zustand/traditional` 引 `createWithEqualityFn`,主入口那个已经把 equalityFn 去掉了。派生值还是放 selector 外面用 `useMemo` 最省心。

React Query 再补两个:`gcTime`(v4 叫 `cacheTime`)默认 5 分钟,是"切回来又请求"的另一个来源,跟 `staleTime: 0` 是两个独立开关,别混着排查;

shandian
shandian 见习用户见习用户 #275 6楼 2026-09-30 20:29
玄墨染:工具兜静态的、约定兜动态的——这条我认,但得给它加个前提:lint 拦得住的只是"能静态看出来"的那半,剩下那半运行时语义问题(竞态、卸载后 setState、…

运行时那半工具兜不住,还有个比"静态看不出来"更硬的原因:React 18 把"卸载后 setState"的那条警告直接删了——以前它至少在控制台喊你一句,现在连喊都不喊,约不成约定就真的没人知道。

所以"约定兜动态"这条我认,但约定得落到改代码时绕不过去的地方,否则三个月后就漂成 wiki 里的一段话。比较省事的做法是把它塞进 PR 模板当三问:effect 有 cleanup 吗?请求带 AbortController 并把 signal 透给 queryFn 吗?Context value 是每次渲染新建的对象吗?能过这三问,你列的竞态、卸载后 setState、Context 不稳定基本都覆盖到了。

顺着 StrictMode 那句补一刀:双跑 effect 不是 bug,是免费的竞态测试。迁移期最常见的错误反应是把它关掉"少发一次请求",等于把没写 cleanup 的 effect 一次性全捂上。建议反过来——迁移全程开着,让它替你把这些全暴露出来。

你那个 gcTime 的点也补完整:它和 staleTime 是串联不是并列。staleTime 管"数据多久算旧",gcTime 管"没组件观察之后多久扔缓存",所以 gcTime 必须 ≥ staleTime 才有意义,否则数据还没旧就被回收了。而"切回来又请求"绝大多数是 refetchOnMount 撞上 staleTime 已过,跟 gcTime 无关——这也是很多人排查方向一开始就跑偏的地方。

另外 exhaustive-deps 真要 disable,就在行尾把理由写死("只依赖 user.id,user 每次新建"),别甩一行裸 disable 走人。

收尾一句:约定的生命力不取决于写得多全,取决于它是不是变成了别人改代码时躲不开的一道门。