有 Vue 基础,学 React 钩子会快很多。核心差别是:
Vue 靠「响应式系统自动追踪依赖」,React 靠「钩子函数手动声明副作用和缓存」。
下面用 Vue 类比,把常用钩子一次讲清。
一、先建立心智模型
Vue React
ref / reactive useState
computed useMemo
watch / watchEffect useEffect
onMounted / onUnmounted useEffect(靠依赖数组控制)
provide / inject useContext
模板 ref useRef
Pinia / Vuex useReducer + useContext(轻量场景)
自定义 Hook(Vue 3 useXxx) 自定义 Hook(useXxx)
关键区别:
· Vue:数据变了,依赖自动重新计算。
· React:数据变了,整个函数组件重新执行,你需要用钩子告诉 React「什么该缓存、什么该执行」。
二、useState ------ 相当于 ref / reactive
作用
声明一个响应式状态,改变它会触发组件重新渲染。
Vue 写法
vue
<script setup>
import { ref } from 'vue';
const count = ref(0);
function add() {
count.value++;
}
</script>
<template>
<button @click="add">{{ count }}</button>
</template>
React 写法
tsx
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
function add() {
setCount(count + 1);
}
return <button onClick={add}>{count}</button>;
}
对比
Vue React
const count = ref(0) const count, setCount = useState(0)
count.value count
count.value++ setCount(count + 1)
自动追踪依赖 手动调用 setXxx 触发更新
使用场景
· 计数器、表单输入、开关状态、列表数据
· 任何需要「变了就刷新页面」的数据
注意
tsx
// ❌ 不能直接改
count++;
// ✅ 用 set 函数
setCount(count + 1);
// ✅ 依赖旧值时用函数式
setCount(prev => prev + 1);
三、useEffect ------ 相当于 watch + 生命周期
这是 React 最重要的钩子,也是 Vue 转 React 最容易懵的。
作用
处理副作用:请求数据、订阅事件、操作 DOM、定时器。
三种形态
tsx
// 1. 每次渲染都执行
useEffect(() => {
console.log('渲染了');
});
// 2. 只在挂载时执行一次(相当于 onMounted)
useEffect(() => {
console.log('挂载');
}, []);
// 3. 依赖变化时执行(相当于 watch)
useEffect(() => {
console.log('count 变了', count);
}, [count]);
清理函数(相当于 onUnmounted)
tsx
useEffect(() => {
const timer = setInterval(() => {
console.log('tick');
}, 1000);
// 组件卸载或依赖变化前执行
return () => {
clearInterval(timer);
};
}, []);
Vue 对比
vue
<script setup>
import { watch, onMounted, onUnmounted } from 'vue';
onMounted(() => {
console.log('挂载');
});
watch(count, (newVal) => {
console.log('count 变了', newVal);
});
onUnmounted(() => {
console.log('卸载');
});
</script>
tsx
useEffect(() => {
console.log('挂载');
return () => console.log('卸载');
}, []);
useEffect(() => {
console.log('count 变了', count);
}, [count]);
使用场景
场景 写法
请求接口 useEffect(() => { fetchData() }, \[\])
监听窗口大小 useEffect(() => { window.addEventListener(...) }, \[\])
定时器 useEffect(() => { const t = setInterval(...); return () => clearInterval(t) }, \[\])
依赖某个值变化 useEffect(() => {...}, keyword)
操作 DOM useEffect(() => { ref.current.focus() }, \[\])
依赖数组规则
tsx
useEffect(() => {...}); // 每次渲染都跑
useEffect(() => {...}, []); // 只跑一次
useEffect(() => {...}, [a]); // a 变了就跑
useEffect(() => {...}, [a,b]); // a 或 b 变了就跑
记住:依赖数组里用到的外部变量都要写进去,否则拿到旧值。
四、useMemo ------ 相当于 computed
作用
缓存计算结果,依赖不变就不重新计算。
Vue 写法
vue
<script setup>
import { ref, computed } from 'vue';
const list = ref([1, 2, 3]);
const total = computed(() => {
console.log('计算了');
return list.value.reduce((a, b) => a + b, 0);
});
</script>
React 写法
tsx
import { useState, useMemo } from 'react';
function App() {
const [list] = useState([1, 2, 3]);
const total = useMemo(() => {
console.log('计算了');
return list.reduce((a, b) => a + b, 0);
}, [list]);
return <div>{total}</div>;
}
对比
Vue React
computed(() => ...) useMemo(() => ..., deps)
自动追踪依赖 手动写依赖数组
值缓存 值缓存
使用场景
· 复杂计算(排序、过滤、统计)
· 避免每次渲染都重新算
· 作为其他 Hook 的依赖时保持引用稳定
注意
tsx
// 简单计算不用 useMemo,反而增加开销
const double = count * 2; // 直接算就行
// 复杂计算才用
const sorted = useMemo(() => bigList.sort(...), [bigList]);
五、useCallback ------ 缓存函数
作用
缓存函数引用,依赖不变就返回同一个函数。
为什么需要?
React 每次渲染都会重新创建函数,如果这个函数传给子组件,子组件会认为 props 变了而重新渲染。
写法
tsx
import { useState, useCallback } from 'react';
function App() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('点击');
}, []);
return <Child onClick={handleClick} />;
}
Vue 对比
Vue 里没有直接对应,因为 Vue 的响应式系统不需要你手动缓存函数。
使用场景
· 函数作为 props 传给 React.memo 包裹的子组件
· 函数作为其他 Hook 的依赖
注意
tsx
// 大部分情况不需要 useCallback
// 只有性能问题出现时才用
useMemo 缓存值,useCallback 缓存函数。
tsx
// 等价
useCallback(fn, deps) === useMemo(() => fn, deps)
六、useRef ------ 相当于模板 ref + 普通变量
作用
-
获取 DOM 元素(像 Vue 的模板 ref)
-
保存不触发渲染的变量
-
获取 DOM
Vue:
vue
<template>
<input ref="inputRef" />
</template>
<script setup>
import { ref, onMounted } from 'vue';
const inputRef = ref(null);
onMounted(() => {
inputRef.value.focus();
});
</script>
React:
tsx
import { useRef, useEffect } from 'react';
function App() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;
}
- 保存变量(改了不重新渲染)
tsx
function App() {
const countRef = useRef(0);
function handleClick() {
countRef.current++;
console.log(countRef.current); // 变了,但页面不刷新
}
return <button onClick={handleClick}>点击</button>;
}
对比
Vue React
const el = ref(null) + 模板 ref="el" const el = useRef(null) + ref={el}
el.value el.current
let timer 普通变量 useRef 保持跨渲染
使用场景
· 操作 DOM:聚焦、滚动、播放视频
· 保存定时器 ID
· 保存上一次的值
· 保存不需要触发渲染的数据
七、useContext ------ 相当于 provide / inject
作用
跨层级传数据,不用一层层 props 传。
Vue 写法
vue
<!-- 父 -->
<script setup>
import { provide } from 'vue';
provide('theme', 'dark');
</script>
<!-- 深层子 -->
<script setup>
import { inject } from 'vue';
const theme = inject('theme');
</script>
React 写法
tsx
import { createContext, useContext } from 'react';
// 1. 创建 Context
const ThemeContext = createContext('light');
// 2. 父组件提供
function App() {
return (
<ThemeContext.Provider value="dark">
<Child />
</ThemeContext.Provider>
);
}
// 3. 深层子组件消费
function Child() {
const theme = useContext(ThemeContext);
return <div>{theme}</div>;
}
对比
Vue React
provide('key', value) <Context.Provider value={...}>
inject('key') useContext(Context)
按 key 取 按 Context 对象取
使用场景
· 主题切换
· 用户登录信息
· 语言国际化
· 全局配置
八、useReducer ------ 相当于简化版 Pinia
作用
集中管理复杂状态逻辑,适合多个状态互相关联。
写法
tsx
import { useReducer } from 'react';
type State = { count: number };
type Action = { type: 'add' } | { type: 'sub' } | { type: 'reset' };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'add':
return { count: state.count + 1 };
case 'sub':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
default:
return state;
}
}
function App() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: 'add' })}>+1</button>
<button onClick={() => dispatch({ type: 'sub' })}>-1</button>
<button onClick={() => dispatch({ type: 'reset' })}>重置</button>
</div>
);
}
对比 Pinia
Pinia useReducer
state state
actions dispatch(action)
直接调用 action 派发 action 对象
使用场景
· 状态逻辑复杂,多个值互相影响
· 状态更新逻辑想集中管理
· 配合 useContext 做轻量全局状态
useState vs useReducer
tsx
// 简单状态用 useState
const [count, setCount] = useState(0);
// 复杂状态用 useReducer
const [state, dispatch] = useReducer(reducer, initialState);
九、自定义 Hook ------ 相当于 Vue 的组合式函数
作用
把可复用逻辑抽出来,名字必须以 use 开头。
Vue 写法
ts
// useCounter.ts
import { ref } from 'vue';
export function useCounter(initial = 0) {
const count = ref(initial);
const add = () => count.value++;
return { count, add };
}
React 写法
tsx
// useCounter.ts
import { useState } from 'react';
export function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
const add = () => setCount(c => c + 1);
return { count, add };
}
// 使用
function App() {
const { count, add } = useCounter(0);
return <button onClick={add}>{count}</button>;
}
使用场景
· 封装请求逻辑 useRequest
· 封装表单 useForm
· 封装本地存储 useLocalStorage
· 封装窗口大小 useWindowSize
常用示例
tsx
// useLocalStorage
function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : initial;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
十、钩子使用规则(必须遵守)
React 钩子有两条铁律:
- 只能在函数组件或自定义 Hook 顶层调用
tsx
// ❌ 不能在条件里
if (cond) {
useState(0);
}
// ❌ 不能在循环里
for (let i = 0; i < 3; i++) {
useState(0);
}
// ✅ 只能在顶层
const [a] = useState(0);
- 不能在普通函数里调用
tsx
// ❌
function helper() {
useState(0);
}
// ✅ 自定义 Hook
function useHelper() {
useState(0);
}
原因:React 靠调用顺序识别每个 Hook,顺序乱了就出错。
十一、Vue 转 React 常见误区
- 直接改 state
tsx
// ❌
count++;
// ✅
setCount(count + 1);
- 以为 setState 是同步的
tsx
setCount(count + 1);
console.log(count); // 还是旧值
想拿新值:
tsx
setCount(prev => {
console.log(prev + 1);
return prev + 1;
});
- useEffect 依赖写不全
tsx
// ❌ count 是旧值
useEffect(() => {
console.log(count);
}, []);
// ✅
useEffect(() => {
console.log(count);
}, [count]);
- 对象/数组 state 直接改
tsx
// ❌
user.name = 'Bob';
setUser(user);
// ✅ 创建新对象
setUser({ ...user, name: 'Bob' });
- 以为 useEffect 等于 watch
useEffect 是渲染后执行,不是数据一变立刻执行,且它处理的是副作用,不是纯计算。
十二、速查表
Hook Vue 对应 作用 常用场景
useState ref / reactive 声明状态 计数器、表单
useEffect watch + 生命周期 副作用 请求、订阅、定时器
useMemo computed 缓存值 复杂计算
useCallback 无 缓存函数 传给子组件的函数
useRef 模板 ref / 普通变量 DOM 引用 / 不渲染变量 聚焦、定时器 ID
useContext provide/inject 跨层传值 主题、用户信息
useReducer Pinia 简化版 复杂状态 多状态关联
自定义 Hook 组合式函数 复用逻辑 请求、表单封装
十三、一句话总结
· useState:声明响应式数据,像 ref。
· useEffect:处理副作用,像 watch + 生命周期。
· useMemo:缓存计算结果,像 computed。
· useCallback:缓存函数引用。
· useRef:拿 DOM 或存不渲染的值。
· useContext:跨层传值,像 provide/inject。
· useReducer:复杂状态管理,像简化版 Pinia。
· 自定义 Hook:复用逻辑,像 Vue 组合式函数。
· 两条铁律:只在顶层调用,只在组件或 Hook 里调用。
Vue 是「自动挡」,React 是「手动挡」,钩子就是你手动控制渲染和副作用的工具。