1. 什么是 Hooks
Hooks 是一组以 use 开头的函数,让你在函数组件中"钩入"(hook into)React 的状态管理和生命周期能力。在 Hooks 出现之前,这些能力只有类组件才有。
Hooks 解决的类组件痛点
- 逻辑复用困难:类组件用 HOC(高阶组件)或 Render Props 复用逻辑,导致"嵌套地狱"
- 生命周期分散:相关逻辑被分散在
componentDidMount、componentDidUpdate、componentWillUnmount中 - this 困惑:类组件中
this的指向让人头疼 - 难以测试:状态和 UI 耦合在类中,单元测试困难
Hooks 的两条铁律(Rules of Hooks)
规则一:只在函数组件顶层调用 Hook,不能在条件语句、循环或嵌套函数中调用。
规则二:只在 React 函数组件或自定义 Hook 中调用 Hook,不能在普通 JS 函数中调用。
违反这两条规则会导致难以排查的 bug。ESLint 插件 eslint-plugin-react-hooks 会自动检测。
2. useState
useState 是最基础的 Hook,让函数组件持有响应式状态——当 state 改变时,React 自动重新渲染组件。
state 的本质
每次组件渲染时,useState 返回当前这次渲染的快照值。state 不是普通变量——它在两次渲染之间被 React 持久化,而普通变量在每次渲染时都会重置。
import { useState } from 'react';
function Counter() {
// 解构:[当前值, setter 函数]
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
{/* setter 调用后,React 调度重渲染 */}
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
函数式更新(基于上次值)
当新 state 依赖于上一个 state 时,应该用函数式更新,避免闭包陷阱(读到过期的 state 值)。
// ❌ 直接用变量 —— 在异步中可能读到过期值
setCount(count + 1);
setCount(count + 1); // count 仍是旧值,结果只 +1,不是 +2
// ✅ 函数式更新 —— 始终基于最新值
setCount(prev => prev + 1);
setCount(prev => prev + 1); // 正确 +2
// 常见场景:toggle
setIsOpen(prev => !prev);
对象/数组 state 的不可变更新
React 通过对比引用判断 state 是否变化。如果直接修改对象/数组(改变内部属性但引用不变),React 不会触发重渲染。必须创建新对象/数组。
const [user, setUser] = useState({ name: 'Alice', age: 25, skills: ['JS'] });
// ❌ 直接修改 —— React 不会重渲染!
user.age = 26;
setUser(user); // 引用未变,React 认为没有变化
// ✅ 展开创建新对象
setUser({ ...user, age: 26 });
// ✅ 数组:不要 push/splice,要创建新数组
const [items, setItems] = useState<string[]>([]);
// 新增
setItems(prev => [...prev, 'new item']);
// 删除(filter 返回新数组)
setItems(prev => prev.filter(item => item !== 'old item'));
// 更新某一项
setItems(prev => prev.map(item =>
item === 'old' ? 'new' : item
));
初始化函数(避免重复计算)
// ❌ 这个函数每次渲染都执行(即使初始值只用一次)
const [state, setState] = useState(computeExpensiveValue());
// ✅ 传入函数 —— 只在首次渲染时执行
const [state, setState] = useState(() => computeExpensiveValue());
const [items, setItems] = useState(() =>
JSON.parse(localStorage.getItem('items') ?? '[]')
);
3. useEffect
useEffect 用于执行副作用(Side Effects)——那些与渲染无关的操作,如数据获取、DOM 操作、订阅事件、设置定时器等。
依赖数组三种情况对比
// 情况1:无依赖数组 —— 每次渲染后都执行
useEffect(() => {
console.log('每次渲染后执行');
}); // ← 没有第二个参数
// 情况2:空数组 —— 只在组件挂载时执行一次
useEffect(() => {
console.log('只在 mount 时执行');
}, []); // ← 空数组
// 情况3:有依赖值 —— 依赖变化时执行
useEffect(() => {
console.log('userId 变化时执行');
}, [userId]); // ← 只在 userId 变化时重新运行
清理函数(Cleanup)
Effect 返回的函数是清理函数——在组件卸载或 Effect 重新运行前执行,用于清理副作用(取消订阅、清除定时器、取消请求等)。
useEffect(() => {
// 订阅事件
const handler = (e: Event) => console.log(e);
window.addEventListener('resize', handler);
// 返回清理函数
return () => {
window.removeEventListener('resize', handler);
};
}, []);
// 数据请求的清理(防止组件卸载后设置 state)
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
const data = await fetchUser(userId);
if (!cancelled) setUser(data); // 已卸载时不设置 state
};
fetchData();
return () => { cancelled = true; };
}, [userId]);
常见错误:deps 缺失
如果 Effect 内部使用了某个变量(state/props),但没有把它加入依赖数组,Effect 会读到过期的旧值——这是"闭包陷阱"。eslint-plugin-react-hooks 的 exhaustive-deps 规则会警告这种情况。
4. useRef
useRef 返回一个 { current: T } 对象,有两种截然不同的用途。
用途一:持有 DOM 引用
import { useRef, useEffect } from 'react';
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// 组件挂载后,DOM 元素已存在,可以操作
inputRef.current?.focus();
}, []);
return <input ref={inputRef} placeholder="自动聚焦" />;
}
用途二:持有可变值(不触发重渲染)
有时你需要在渲染之间保存一个值,但改变它时不想触发重渲染(如计时器 ID、上一次的 props 值)。这时用 useRef,而不是 useState。
function Timer() {
const [count, setCount] = useState(0);
const timerIdRef = useRef<ReturnType<typeof setInterval> | null>(null);
const start = () => {
timerIdRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
};
const stop = () => {
if (timerIdRef.current) clearInterval(timerIdRef.current);
};
return (
<div>
<p>{count}</p>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
</div>
);
}
| 特性 | useState | useRef |
|---|---|---|
| 触发重渲染 | ✅ 每次更新都重渲染 | ❌ 更改 current 不触发 |
| 渲染时的值 | 每次渲染的快照值 | 始终是最新的可变值 |
| 适用场景 | UI 依赖的数据 | 定时器 ID、DOM 引用、计数器 |
5. useMemo 与 useCallback
记忆化(Memoization):缓存计算结果或函数引用,只有依赖变化时才重新计算,避免不必要的重计算或重渲染。
useMemo — 缓存计算结果
import { useMemo, useState } from 'react';
function ProductList({ products, searchQuery }: Props) {
// 每次渲染都会重新过滤(如果 products 很多,很慢)
// const filtered = products.filter(p => p.name.includes(searchQuery));
// ✅ useMemo 缓存:只有 products 或 searchQuery 变化时才重新计算
const filtered = useMemo(
() => products.filter(p => p.name.includes(searchQuery)),
[products, searchQuery] // 依赖数组
);
return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
useCallback — 缓存函数引用
每次渲染都会创建新的函数对象,导致接收该函数作为 prop 的子组件总是重渲染(引用不同了)。useCallback 让函数引用稳定。
import { useCallback, memo } from 'react';
// 子组件用 React.memo 包裹 —— 只有 props 变化才重渲染
const Button = memo(({ onClick, label }: ButtonProps) => {
console.log(`渲染 ${label}`);
return <button onClick={onClick}>{label}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// ❌ 每次 Parent 渲染,handleClick 都是新函数 → Button 也重渲染
// const handleClick = () => console.log('clicked');
// ✅ useCallback 缓存函数 —— 依赖不变时,返回同一个函数引用
const handleClick = useCallback(() => {
console.log('clicked, count is', count);
}, [count]);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<Button onClick={handleClick} label="Click me" />
</div>
);
}
过度优化的危害:useMemo/useCallback 本身有开销(缓存存储、依赖比较)。不要无脑加。只在实测存在性能问题时再加,或者:计算非常昂贵(如大数据排序),以及需要配合 React.memo 使用时。
6. useContext
Context 用于在组件树中共享数据,避免逐层传递 Props(Props Drilling)。主题、语言设置、当前登录用户等适合放 Context。
import { createContext, useContext, useState } from 'react';
// 1. 创建 Context(提供默认值)
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme(): void;
}
const ThemeContext = createContext<ThemeContextType>({
theme: 'dark',
toggleTheme: () => {},
});
// 2. Provider 包裹子树,提供值
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('dark');
const toggleTheme = () => setTheme(t => t === 'light' ? 'dark' : 'light');
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// 3. 在任意子组件中消费
function Header() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<header className={`header-${theme}`}>
<button onClick={toggleTheme}>切换主题</button>
</header>
);
}
7. useReducer
当组件状态逻辑复杂(多个子值、下一状态依赖上一状态)时,用 useReducer 比多个 useState 更清晰。它的模式与 Redux 相同:action → reducer → new state。
import { useReducer } from 'react';
// 定义 state 和 action 类型
interface CartState {
items: CartItem[];
totalAmount: number;
}
type CartAction =
| { type: 'ADD_ITEM'; payload: CartItem }
| { type: 'REMOVE_ITEM'; payload: string }
| { type: 'CLEAR' };
// Reducer 函数:(currentState, action) => newState
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD_ITEM':
return {
items: [...state.items, action.payload],
totalAmount: state.totalAmount + action.payload.price,
};
case 'REMOVE_ITEM':
const removed = state.items.find(i => i.id === action.payload);
return {
items: state.items.filter(i => i.id !== action.payload),
totalAmount: state.totalAmount - (removed?.price ?? 0),
};
case 'CLEAR':
return { items: [], totalAmount: 0 };
default:
return state;
}
}
function ShoppingCart() {
const [cart, dispatch] = useReducer(cartReducer, { items: [], totalAmount: 0 });
return (
<div>
<p>总价: ¥{cart.totalAmount}</p>
<button onClick={() => dispatch({ type: 'CLEAR' })}>清空</button>
</div>
);
}
8. 自定义 Hook
自定义 Hook 是以 use 开头的普通函数,内部可以使用其他 Hook。它是提取可复用逻辑的最佳方式,比 HOC 和 Render Props 更简洁直观。
useFetch — 数据获取
import { useState, useEffect } from 'react';
interface FetchState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
function useFetch<T>(url: string): FetchState<T> {
const [state, setState] = useState<FetchState<T>>({
data: null, loading: true, error: null
});
useEffect(() => {
let cancelled = false;
setState({ data: null, loading: true, error: null });
fetch(url)
.then(r => r.json())
.then(data => { if (!cancelled) setState({ data, loading: false, error: null }); })
.catch(err => { if (!cancelled) setState({ data: null, loading: false, error: err.message }); });
return () => { cancelled = true; };
}, [url]);
return state;
}
// 使用 —— 逻辑完全复用,组件非常简洁
function UserProfile({ userId }: { userId: string }) {
const { data: user, loading, error } = useFetch<User>(
`/api/users/${userId}`
);
if (loading) return <div>加载中...</div>;
if (error) return <div>错误: {error}</div>;
return <div>{user?.name}</div>;
}
useLocalStorage — 持久化状态
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch { return initialValue; }
});
const setStoredValue = (newValue: T) => {
setValue(newValue);
localStorage.setItem(key, JSON.stringify(newValue));
};
return [value, setStoredValue] as const;
}
// 用起来和 useState 一样,但数据会持久化到 localStorage
const [theme, setTheme] = useLocalStorage('theme', 'dark');
自定义 Hook vs 工具函数:如果逻辑内部用了 Hook(useState/useEffect 等),就必须封装为自定义 Hook(use 前缀);如果只是普通逻辑计算(不用 Hook),封装为普通工具函数即可。
9. Hook 的底层原理
理解 Hook 的底层,才能真正明白"为什么不能在条件语句中使用 Hook"这条规则。
链表结构
React 在每个组件实例上维护一条Hook 链表。每次调用 useState、useEffect 等 Hook,都会按顺序在这条链表上追加一个节点。
// 第一次渲染:依次创建链表节点
const [name, setName] = useState(''); // Hook #1
const [age, setAge] = useState(0); // Hook #2
useEffect(() => { /* ... */ }, []); // Hook #3
// 第二次渲染:按相同顺序读取链表节点
const [name, setName] = useState(''); // 读 Hook #1 → 'Alice'
const [age, setAge] = useState(0); // 读 Hook #2 → 25
useEffect(() => { /* ... */ }, []); // 读 Hook #3
React 完全依赖 Hook 调用的顺序来关联节点。如果把 Hook 放在条件语句中,某次渲染时跳过了某个 Hook,链表顺序就乱了——后续所有 Hook 都会读到错误的节点。这就是为什么有"只能在顶层调用"这条规则。
永远不要这样做:
if (isLoggedIn) {
const [user, setUser] = useState(null); // ❌ 条件中的 Hook!
}
本章小结:Hooks 是现代 React 的核心。useState 管理 UI 状态,useEffect 处理副作用(记得清理),useRef 持有 DOM 引用或可变值,useMemo/useCallback 是性能优化工具(按需使用),自定义 Hook 是逻辑复用的最佳方案。