Hook 是 React 16.8 引入的一类特殊函数,命名以 use 开头。它让函数组件拥有了原本只有类组件才有的能力:状态管理、生命周期、跨组件共享逻辑。
为什么需要 Hook
React 16.8 之前,函数组件只能做纯渲染,没有状态、没有生命周期。要用这些功能必须写类组件,但类组件存在几个痛点:
this绑定容易出错- 生命周期方法(
componentDidMount/componentDidUpdate)把不相关的逻辑强行塞在一起 - 状态逻辑难以在组件之间复用
Hook 彻底解决了这些问题,现代 React 项目基本全面使用函数组件 + Hook。
Hook 使用规则
这两条规则必须严格遵守,否则会导致 Bug。
- 只在函数组件或自定义 Hook 的顶层调用 Hook,不能在条件语句、循环、嵌套函数中调用
- 只在 React 函数中调用 Hook,不能在普通 JS 函数中调用
// ❌ 错误:在条件语句中调用
if (condition) {
const [value, setValue] = useState(0);
}
// ✅ 正确:在顶层调用
const [value, setValue] = useState(0);
if (condition) {
setValue(1);
}
内置 Hook
useState
管理组件的局部状态。
const [state, setState] = useState(initialValue);
state:当前状态值setState:更新状态的函数,调用后触发重渲染initialValue:初始值,也可以传入一个函数(懒初始化)
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>计数:{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(prev => prev - 1)}>-1</button>
</div>
);
}
函数式更新:当新状态依赖上一个状态时,推荐传函数给 setState,避免闭包陷阱:
setCount(prev => prev + 1); // ✅ 推荐
setCount(count + 1); // ⚠️ 在异步场景可能拿到旧值
useEffect
处理副作用:数据请求、事件订阅、手动操作 DOM、定时器等。
useEffect(() => {
// 副作用逻辑
return () => {
// 清理函数(可选),在组件卸载或下次 effect 执行前调用
};
}, [dependency1, dependency2]);
依赖数组控制执行时机:
| 写法 | 执行时机 |
|---|---|
| 省略依赖数组 | 每次渲染后都执行 |
[] |
只在组件挂载时执行一次 |
[a, b] |
挂载时 + a 或 b 变化时执行 |
useEffect(() => {
document.title = `计数:${count}`;
}, [count]); // count 变化时更新标题
useEffect(() => {
const timer = setInterval(() => {
console.log("tick");
}, 1000);
return () => clearInterval(timer); // 清理定时器
}, []);
常见错误:遗漏依赖
// ❌ 错误:count 没加入依赖,始终读到初始值 0
useEffect(() => {
console.log(count);
}, []);
// ✅ 正确
useEffect(() => {
console.log(count);
}, [count]);
useContext
读取 React Context 的值,实现跨层级组件通信,避免逐层传递 Props(prop drilling)。
const value = useContext(MyContext);
// 1. 创建 Context
const ThemeContext = React.createContext("light");
// 2. 提供 Context(通常在顶层组件)
function App() {
return (
<ThemeContext.Provider value="dark">
<Page />
</ThemeContext.Provider>
);
}
// 3. 任意子组件消费
function Button() {
const theme = useContext(ThemeContext);
return <button className={theme}>Click</button>;
}
useRef
持有一个在渲染之间保持稳定的可变引用,修改 .current 不触发重渲染。
const ref = useRef(initialValue);
// ref.current === initialValue
用途 1:访问 DOM 元素
function FocusInput() {
const inputRef = useRef(null);
return (
<>
<input ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>聚焦</button>
</>
);
}
用途 2:保存不需要触发渲染的值(如定时器 ID、上一次的值)
function Timer() {
const timerRef = useRef(null);
const start = () => {
timerRef.current = setInterval(() => console.log("tick"), 1000);
};
const stop = () => clearInterval(timerRef.current);
return (
<>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
</>
);
}
useMemo
缓存计算结果,只有依赖变化时才重新计算,避免每次渲染都执行昂贵的计算。
const result = useMemo(() => expensiveCalculation(a, b), [a, b]);
function ProductList({ products, filter }) {
const filtered = useMemo(
() => products.filter(p => p.category === filter),
[products, filter]
);
return filtered.map(p => <Product key={p.id} {...p} />);
}
不要过度使用
useMemo。只在计算确实耗时时使用,否则反而增加开销。
useCallback
缓存函数引用,只有依赖变化时才返回新函数。常配合 React.memo 使用,避免子组件因函数引用变化而不必要地重渲染。
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);
// 子组件用 React.memo 包裹,只有 props 变化才重渲染
const Button = React.memo(({ onClick, label }) => {
return <button onClick={onClick}>{label}</button>;
});
function Parent({ id }) {
// 没有 useCallback:每次 Parent 渲染都生成新函数 → Button 总是重渲染
// 有 useCallback:id 不变时函数引用稳定 → Button 不重渲染
const handleClick = useCallback(() => {
console.log(id);
}, [id]);
return <Button onClick={handleClick} label="点击" />;
}
useReducer
useState 的替代方案,适合状态逻辑复杂或多个子状态相互关联的场景。思路与 Redux 类似。
const [state, dispatch] = useReducer(reducer, initialState);
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return { count: 0 };
default:
throw new Error("未知 action");
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>计数:{state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+1</button>
<button onClick={() => dispatch({ type: "decrement" })}>-1</button>
<button onClick={() => dispatch({ type: "reset" })}>重置</button>
</div>
);
}
useLayoutEffect
与 useEffect 用法相同,但执行时机不同:
| Hook | 执行时机 |
|---|---|
useEffect |
浏览器完成绘制之后(异步) |
useLayoutEffect |
DOM 更新完成、浏览器绘制之前(同步) |
适用场景:需要在浏览器绘制前读取/修改 DOM(如测量元素尺寸、避免闪烁)。大多数情况优先使用 useEffect。
自定义 Hook
自定义 Hook 是将可复用的状态逻辑提取成独立函数的方式,函数名以 use 开头。
// 自定义 Hook:封装数据请求逻辑
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
// 在任意组件中复用
function UserProfile({ userId }) {
const { data, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <p>加载中...</p>;
if (error) return <p>出错了</p>;
return <p>{data.name}</p>;
}
自定义 Hook 的优势:
- 逻辑与 UI 分离,组件代码更清晰
- 同一逻辑在多个组件中复用,不需要 HOC 或 Render Props
- 便于单独测试
Hook 对比类组件生命周期
| 类组件 | Hook 等价写法 |
|---|---|
componentDidMount |
useEffect(() => {}, []) |
componentDidUpdate |
useEffect(() => {}, [dep]) |
componentWillUnmount |
useEffect 返回的清理函数 |
shouldComponentUpdate |
React.memo + useMemo / useCallback |