学完这篇,你能把 Vue 项目迁到 React 时不靠"翻译语法"硬搬,而是先换脑子、再动手,并绕开那批最常见的坑。
第一步:先把"响应式"这条腿换掉
Vue 用 ref(0),改的时候 .value++,视图自己跟着变,因为 Proxy 在替你追踪依赖。React 没有追踪这回事,它只有一条规则:调用 setter → 组件函数整体重跑 → 生成新的 JSX。
// Vue
const count = ref(0); count.value++;
// React
const [count, setCount] = useState(0);
setCount(count + 1);
由此推出第一个大坑:不要原地改数据。数组 push、对象改属性,引用没变,React 认为没变化,不重渲染。
// ❌ list.push(item); setList(list);
// ✅ setList([...list, item]);
注意:setCount(count + 1) 连写两次只会加 1,因为闭包读的是同一次渲染的旧值。要写成 setCount(c => c + 1)。
第二步:computed / watch → useMemo / useEffect
Vue 的 computed 自动收集依赖,React 要你手写依赖数组。判断标准很简单:
- 渲染期间算出来的值 →
useMemo
- 出了渲染之外的副作用(请求、订阅、定时器、改 DOM)→
useEffect
const total = useMemo(
() => list.reduce((sum, i) => sum + i.price, 0),
[list]
);
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id); // 相当于 onUnmounted
}, []); // 空数组 = 只跑一次(约等于 onMounted)
注意:依赖数组漏写会读到旧值(stale closure),依赖里塞整个对象又会导致每次都重跑。依赖只放"真正用到的原始值"。
第三步:模板语法 → JSX
| Vue | React |
|---|
v-if="ok" | {ok && <X />} 或三元 |
v-for="i in list" | {list.map(i => <Row key={i.id} />)} |
:class="{active: on}" | className={on ? 'active' : ''} |
@click="fn" | onClick={fn} |
v-model="v" | value={v} onChange={e => setV(e.target.value)} |
:style="{color:'red'}" | style={{ color: 'red' }} |
注意:{count && <X />} 当 count 为 0 时页面上会渲染出一个孤零零的 0。数字做条件判断请写 count > 0 && ...。
注意:class 必须写 className,for 必须写 htmlFor;忘了给 key 会警告,用数组下标当 key 在可排序列表里会导致状态错位。
第四步:组件通信换个名字
Vue 的 emit,在 React 里就是往子组件传一个函数;slot 对应 props.children;provide/inject 对应 createContext + useContext;defineExpose 基本没有等价物,非要用就 useImperativeHandle。
// 子组件
function Child({ onPick }) {
return <button onClick={() => onPick(1)}>选 1</button>;
}
第五步:状态管理挑个对味的
Pinia 用户迁过来最顺手的是 Zustand,都是 hook 式、几乎没有样板代码:
const useStore = create(set => ({
count: 0,
inc: () => set(s => ({ count: s.count + 1 })),
}));
如果你原来重度依赖 Vuex 的 mutation/action 分层,那就上 Redux Toolkit,别硬套 Zustand。
第六步:把生命周期对照表贴在显示器边上
| Vue | React |
|---|
onMounted | useEffect(fn, []) |
onUnmounted | useEffect 里 return 的清理函数 |
onUpdated | useEffect(fn)(无依赖数组,慎用) |
watch(prop, cb) | useEffect(cb, [prop]) |
踩坑清单(照着排查)
- 开发环境
useEffect 跑两次:React 18 StrictMode 故意为之,用来暴露清理函数没写全的问题。副作用要幂等,return 里把订阅、定时器、请求 abort 都收干净。
- 定时器/事件监听里读到旧 state:用
useRef 存一份最新值,或把依赖补全。
useEffect 直接传 async 函数:会返回 Promise 报错。要在内部定义 async function run() 再调用。
- 受控输入框打不出字:给了
value 却忘了 onChange。
setState 后立刻读 state:读到的是旧值,React 是批处理的。要在新值上用,就放进 useEffect 或直接用计算出的变量。
- 满屏
useCallback/useMemo:只在确实传给 React.memo 子组件、或作为其他 hook 依赖时才包,否则是纯负担。
- 列表 key 用 index:插入、删除、排序时全部错位,用后端返回的 id。
小结
- 思维上只换一件事:从"数据变 → 框架追踪 → 局部更新"换成"调 setter → 组件重跑 → 出新 JSX"。
- 数据永远返回新引用,不原地改。
- 计算用
useMemo,副作用用 useEffect,依赖数组是命门。
- JSX 里
className、htmlFor、key、受控组件这四处最容易翻车。
- 组件通信 = props 向下 + 函数向上 + Context 跨层,没有 emit 和 slot。
- 迁移顺序建议:先搭好路由和状态管理骨架,再一个页面一个页面搬,别做逐行翻译。