Hooks 是 React 16.8 引入的革命性特性,它让函数组件拥有了状态管理和副作用处理能力,彻底改变了 React 的开发模式。本章将深入讲解日常开发中最常用的 Hooks,覆盖基础用法、最佳实践和常见陷阱。
8.1 基础 Hooks
这五个 Hooks 是每个 React 开发者都必须熟练掌握的核心工具,它们构成了函数组件 90% 以上的逻辑。
useState:状态管理基石
useState 为函数组件添加内部状态。当状态更新时,组件会重新渲染。
基本用法:
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
点击次数:{count}
</button>
);
}
函数式更新: 当新状态依赖于旧状态时,应该使用函数式更新以确保正确性:
setCount(prevCount => prevCount + 1);
为什么需要这样?因为 React 可能会批量处理状态更新,如果直接使用 setCount(count + 1),多次快速点击可能都基于同一次旧值,导致只增加一次。函数式更新保证了每次更新都基于最新状态。
初始化代价较高时: 可以传入一个函数,它只在首次渲染时执行:
const [data, setData] = useState(() => {
return expensiveComputation();
});
更新对象/数组时的不可变性: 状态应该被视为不可变的,必须创建新对象/数组,而不是在原内存上修改:
// ❌ 错误:直接修改
user.name = 'new name';
setUser(user);
// ✅ 正确:创建新对象
setUser({ ...user, name: 'new name' });
useEffect:副作用与生命周期管理
useEffect 用于处理副作用:数据请求、订阅、定时器、手动 DOM 操作等。它在渲染完成后异步执行,不会阻塞浏览器渲染。
基本模式:
useEffect(() => {
// 副作用逻辑(如请求数据、注册监听)
return () => {
// 清理函数(如取消订阅、清除定时器)
};
}, [依赖项数组]);
三种常见用法:
- 不传依赖项数组 → 每次渲染后都执行
useEffect(() => {
document.title = `点击次数:${count}`;
});
- 空数组
[]→ 仅在首次挂载后执行一次(模拟 componentDidMount)
useEffect(() => {
fetch('/api/user').then(/*...*/);
}, []);
- 指定依赖 → 仅当依赖项变化时重新执行
useEffect(() => {
fetch(`/api/user/${userId}`).then(/*...*/);
}, [userId]);
清理函数的作用: 防止内存泄漏。例如清除定时器、取消待完成的数据请求、移除事件监听:
useEffect(() => {
const timer = setInterval(() => { /*...*/ }, 1000);
return () => clearInterval(timer);
}, []);
React 18 开发模式下的双重执行: 在严格模式(StrictMode)下,React 会故意两次挂载组件(只发生在开发环境),帮助你提前发现副作用清理问题。如果清理函数正确,两次执行不会产生 bug。
useRef:跨渲染周期的持久引用
useRef 返回一个可变的 ref 对象,其 .current 属性在整个组件生命周期内保持不变,且修改不会触发重新渲染。它有两个主要用途:
1. 获取 DOM 元素引用
function TextInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} />;
}
2. 保存跨渲染的变量
function Timer() {
const intervalRef = useRef();
const [count, setCount] = useState(0);
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(intervalRef.current);
}, []);
return <div>{count}</div>;
}
这里用 ref 保存定时器 id,因为它不会被闭包捕获旧值,清理函数总能拿到正确的 id。
常见场景:保存上一次的值
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
useContext:跨层级传递数据
useContext 用于读取 React.createContext 提供的上下文值,无需手动通过 props 层层传递。
创建并使用 Context:
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
const theme = useContext(ThemeContext);
return <div className={theme}>当前主题:{theme}</div>;
}
当 Provider 的 value 变化时,所有消费该 Context 的组件都会重新渲染。这是“性能隐患”的根源——如果 value 是一个对象,且父组件每次渲染都创建新对象,会导致所有消费者不必要的重渲染。解决方案是使用 useMemo 缓存 value:
const value = useMemo(() => ({ theme, fontSize }), [theme, fontSize]);
8.2 性能优化 Hooks
useMemo:缓存计算结果
useMemo 缓存一个计算量大的“值”,只有当依赖项变化时才重新计算。
function OrderSummary({ items }) {
const totalPrice = useMemo(() => {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}, [items]);
return <div>总价:{totalPrice}</div>;
}
items 不变则直接返回上次缓存的结果,避免每次渲染都执行 reduce。注意: 不要无脑使用 useMemo,它本身也有内存开销,只应用于真正需要避免重复计算的场景,比如复杂排序、大数据量过滤等。简单计算直接用 js 逻辑即可。
useCallback:缓存函数引用
useCallback 与 useMemo 类似,但它缓存一个“函数”,只有当依赖项变化时才会返回新函数。主要用于避免子组件不必要的重渲染。
当把函数作为 prop 传给使用 React.memo 的子组件时,如果每次渲染都创建新函数,子组件会认为 prop 变化了而重新渲染:
const handleClick = useCallback(() => {
doSomething(count);
}, [count]);
常见误区: 很多开发者一遇到函数就套 useCallback,但其实只有当子组件使用了 React.memo 且函数作为 prop 时才需要。对于没有 memo 的组件,useCallback 是徒劳的。
useMemo 与 useCallback 的选择
- 需要缓存值(对象、数组、计算结果)→
useMemo - 需要缓存函数(尤其要传给 memo 子组件或作为其他 Hooks 依赖)→
useCallback - 两者本质上等价:
useCallback(fn, deps)相当于useMemo(() => fn, deps)
8.3 进阶 Hooks
useReducer:复杂状态管理的利器
当状态逻辑较复杂(多个子值、下一个状态依赖前一个状态)或涉及多种操作时,useReducer 比 useState 更清晰。它遵循 Redux 风格。
const initialState = { count: 0 };
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();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</>
);
}
当有多个 useState 相互关联,或者状态更新逻辑分散在多个回调中时,使用 useReducer 可以把更新逻辑集中到 reducer 中,提高可维护性。
useLayoutEffect:同步执行的副作用
useEffect 是异步执行的,不会阻塞浏览器绘制;而 useLayoutEffect 则在 DOM 变更之后同步执行,在浏览器绘制之前完成。几乎 99% 的场景应该用 useEffect,仅在需要读取 DOM 布局并同步修改、避免闪烁时才用 useLayoutEffect。
例如:获取滚动位置或元素尺寸后立即调整 UI:
useLayoutEffect(() => {
const rect = ref.current.getBoundingClientRect();
setTooltipPosition({ x: rect.right, y: rect.top });
}, []);
如果使用 useEffect 可能会先渲染默认位置再跳变(闪屏)。
useImperativeHandle:暴露自定义组件方法
通常配合 forwardRef 一起使用,让父组件能够调用子组件内部定义的方法。应尽量少用,因为它打破了数据流单向性。典型场景:输入框焦点控制。
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} />;
});
// 父组件
function App() {
const inputRef = useRef();
return (
<>
<FancyInput ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>聚焦</button>
</>
);
}
useTransition 和 useDeferredValue:并发渲染优先级控制
React 18 引入了并发特性,这两个 Hooks 用于区分紧急更新和非紧急更新。
useTransition: 返回一个标志 isPending 和一个 startTransition 函数。将耗时的更新包裹在 startTransition 中,React 会将它标记为非紧急,在渲染过程中可以被更紧急的更新(如输入)打断。
function SearchPage() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
// 紧急更新:输入框值
setQuery(e.target.value);
// 非紧急更新:搜索结果
startTransition(() => {
setSearchResults(filterData(e.target.value));
});
};
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<ResultList data={searchResults} />
</div>
);
}
useDeferredValue: 直接延迟一个值的更新,常用于当父组件传递一个频繁变化的值给子组件,而子组件渲染开销大时。
const deferredQuery = useDeferredValue(query);
return <SlowList query={deferredQuery} />;
React 会优先更新输入框,等主线程空闲后再更新 SlowList,保证交互流畅。
8.4 自定义 Hooks 设计与封装
自定义 Hooks 是复用状态逻辑的最佳方式。它本质上是一个以 use 开头的函数,内部可以调用其他 Hooks。
封装原则:
- 单一职责:一个 Hook 只做一件事。
- 可配置:通过参数灵活控制行为。
- 返回明确:通常返回
[state, action]或{ data, loading, error }等清晰结构。 - 命名规范:必须以
use开头,这样 React 才能检测到调用其他 Hooks。
示例1:数据请求 Hook
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(res => res.json())
.then(json => {
if (!cancelled) {
setData(json);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}
使用:const { data, loading, error } = useFetch('/api/user'); 即可在任何组件中复用请求逻辑。
示例2:表单输入绑定
function useInput(initialValue) {
const [value, setValue] = useState(initialValue);
const onChange = useCallback((e) => setValue(e.target.value), []);
const reset = useCallback(() => setValue(initialValue), [initialValue]);
return { value, onChange, reset };
}
常见坑点:
- 不要在条件语句或循环中调用自定义 Hook(因为它内部调用了 React Hooks,必须遵循 Hooks 规则)。
- 当 Hook 返回函数(如
dispatch、reset)时,尽量用useCallback包装以确保引用稳定,避免下游组件无谓重渲染。 - 注意清理副作用,在
useEffect返回的清理函数中取消请求、清除定时器等。
自定义 Hooks 的繁荣是 React 社区活力的体现,掌握它可以让你写出高度解耦、可测试的状态逻辑,是实现“逻辑复用”的银弹。