本文系统梳理 React 中 8 个最常用的核心 Hooks:
useState、useReducer、useContext、useEffect、useRef、useImperativeHandle、useMemo、useCallback。从底层运行机制到高阶实战用法,配合完整可运行的代码块,助你彻底掌握函数式组件的开发精髓。
前言
React 16.8 正式引入 Hooks,使得函数组件拥有了类组件的全部能力(状态、生命周期、上下文等),同时摒弃了类组件中 this 指向混乱、逻辑复用困难(HOC / Render Props)等问题。Hooks 的本质是将组件状态逻辑与视图逻辑进行解耦 ,其底层依赖 React 内部的 Fiber 架构 ,通过链表(memoizedState)来按顺序存储各个 Hook 的状态值。
核心铁律(必须遵守) :
- 只在 React 函数组件或自定义 Hook 中调用 Hooks。
- 只在顶层调用 Hooks(不在循环、条件或嵌套函数中),以保证 Hooks 的调用顺序在每次渲染中一致。
1. useState ------ 响应式状态的基石
原理
useState 是 React 内部状态管理的最小单元。在 Fiber 节点上,状态以单向链表的形式存储。每次渲染时,React 根据 Hook 的调用顺序依次从链表上读取对应的状态值。setState 会触发组件重新渲染,并生成新的 Fiber 树。
作用
为函数组件添加内部状态,状态改变时 UI 自动更新。
语法
jsx
scss
const [state, setState] = useState(initialState);
- 支持函数式更新:
setState(prev => prev + 1)可避免因闭包导致的状态过期问题。
示例
jsx
ini
import { useState } from 'react';
function UserForm() {
const [username, setUsername] = useState('');
const [age, setAge] = useState(18);
const handleSubmit = () => {
alert(`用户名:${username},年龄:${age}`);
};
return (
<div>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="请输入用户名"
/>
<input
type="number"
value={age}
onChange={(e) => setAge(Number(e.target.value))}
/>
<button onClick={handleSubmit}>提交</button>
</div>
);
}
2. useReducer ------ 复杂状态逻辑的治理专家
原理
与 useState 共享底层状态存储机制,但将状态更新逻辑集中到 reducer 纯函数中。通过 dispatch 派发 action,reducer 根据 action.type 计算出新状态。这种模式源自 Redux,让状态变化可预测、可追踪。
作用
- 当状态更新逻辑包含多个子值或复杂转换(如购物车、表单校验)时,比
useState更具可读性。 - 适合状态更新依赖先前状态,且操作类型多样(增、删、改、重置)的场景。
语法
jsx
scss
const [state, dispatch] = useReducer(reducer, initialArg, init);
示例:购物车数量管理
jsx
javascript
import { useReducer } from 'react';
// 1. 定义 reducer
function cartReducer(state, action) {
switch (action.type) {
case 'ADD':
return { ...state, count: state.count + 1 };
case 'SUBTRACT':
return { ...state, count: Math.max(0, state.count - 1) };
case 'RESET':
return { ...state, count: 0 };
default:
return state;
}
}
function ShoppingCart() {
const [state, dispatch] = useReducer(cartReducer, { count: 0 });
return (
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
<button onClick={() => dispatch({ type: 'SUBTRACT' })}>-</button>
<span>数量:{state.count}</span>
<button onClick={() => dispatch({ type: 'ADD' })}>+</button>
<button onClick={() => dispatch({ type: 'RESET' })}>重置</button>
</div>
);
}
3. useContext ------ 跨层级数据传递的直通车
原理
useContext 基于 React 的 Context API 。当上层组件通过 <Provider value={...}> 提供数据时,React 会在 Fiber 节点上维护一个 context 链。下层调用 useContext 会向上查找最近的 Provider,并订阅其值。当 Provider 的 value 变化时,所有订阅该 Context 的组件会触发更新。
作用
彻底解决 Props Drilling(属性钻取)问题,让数据在组件树中无障碍传递,常用于主题、语言环境、用户鉴权信息等全局数据。
语法
jsx
ini
const MyContext = React.createContext(defaultValue);
const value = useContext(MyContext);
示例:全局主题切换
jsx
javascript
import { createContext, useContext, useState } from 'react';
// 1. 创建 Context
const ThemeContext = createContext({ theme: 'light', toggleTheme: () => {} });
// 2. 提供者组件
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme((t) => (t === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// 3. 深层子组件消费
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
const isDark = theme === 'dark';
return (
<button
onClick={toggleTheme}
style={{
background: isDark ? '#333' : '#fff',
color: isDark ? '#fff' : '#000',
padding: '8px 16px',
border: '1px solid #ccc',
cursor: 'pointer',
}}
>
当前主题:{theme},点击切换
</button>
);
}
function Toolbar() {
return <ThemedButton />; // 中间组件无需透传任何 props
}
// 4. 使用
export default function App() {
return (
<ThemeProvider>
<Toolbar />
</ThemeProvider>
);
}
4. useEffect ------ 副作用与生命周期的统一管理
原理
useEffect 的调度基于 Fiber 的 Effect List 。在浏览器完成布局与绘制 之后,React 会异步执行 Effect 函数(默认)。通过依赖数组,React 使用 Object.is 比较前后依赖项,决定是否跳过执行。清理函数会在组件卸载或下一次 Effect 执行之前调用,用于清除定时器、取消订阅等。
作用
- 替代类组件生命周期:
componentDidMount(空依赖)、componentDidUpdate(指定依赖)、componentWillUnmount(清理函数)。 - 执行数据获取、手动 DOM 操作、订阅事件等副作用。
语法
jsx
scss
useEffect(() => {
// 副作用逻辑
return () => {
// 清理逻辑(可选)
};
}, [dependencies]);
示例:获取用户数据并设置定时器
jsx
javascript
import { useState, useEffect } from 'react';
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true; // 防止组件卸载后依然 setState
// 模拟异步请求
const fetchData = async () => {
try {
const response = await new Promise((resolve) =>
setTimeout(() => resolve({ name: '王五', age: 30 }), 2000)
);
if (isMounted) {
setUser(response);
setLoading(false);
}
} catch (error) {
if (isMounted) setLoading(false);
}
};
fetchData();
// 清理函数:组件卸载时取消挂载标记
return () => {
isMounted = false;
};
}, []); // 仅在挂载时执行一次
if (loading) return <p>加载中...</p>;
return (
<div>
<h2>用户信息</h2>
<p>姓名:{user.name}</p>
<p>年龄:{user.age}</p>
</div>
);
}
5. useRef ------ 存储可变数据与 DOM 引用
原理
useRef 返回一个 { current: initialValue } 对象。该对象在组件的整个生命周期内内存地址恒定 (类似类组件的实例属性)。修改 current 不会触发组件重新渲染,这使得它非常适合存储"不影响视图"的可变数据。
作用
- 获取 DOM 元素的引用(如聚焦、测量尺寸)。
- 存储上一次渲染的值、定时器 ID、WebSocket 实例等跨渲染周期数据。
语法
jsx
ini
const refContainer = useRef(initialValue);
// 使用:refContainer.current
示例一:自动聚焦输入框
jsx
javascript
import { useRef, useEffect } from 'react';
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} placeholder="页面加载后自动聚焦" />;
}
示例二:保存计时器 ID 并清除
jsx
javascript
import { useRef, useState } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
const timerIdRef = useRef(null);
const start = () => {
if (timerIdRef.current) return; // 防止重复启动
timerIdRef.current = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
};
const stop = () => {
clearInterval(timerIdRef.current);
timerIdRef.current = null;
};
return (
<div>
<p>计时:{seconds} 秒</p>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
</div>
);
}
6. useImperativeHandle ------ 封装子组件的命令式接口
原理
useImperativeHandle 必须与 forwardRef 配合。父组件传递的 ref 对象会被子组件接收,useImperativeHandle 允许子组件自定义 挂载到 ref.current 上的内容(通常是一个方法对象)。这通过劫持 ref 的映射过程实现,防止父组件直接操作子组件的 DOM 或内部状态,增强封装性。
作用
- 暴露特定方法给父组件(如
focus、reset、validate)。 - 控制父组件对子组件的访问权限,避免破坏子组件的内部逻辑。
语法
jsx
scss
useImperativeHandle(ref, () => ({
// 返回暴露的方法
}), [dependencies]);
示例:父组件调用子组件表单重置与聚焦
jsx
javascript
import { forwardRef, useImperativeHandle, useRef, useState } from 'react';
// 子组件
const CustomInput = forwardRef((props, ref) => {
const [value, setValue] = useState('');
const inputRef = useRef(null);
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
reset: () => {
setValue('');
inputRef.current.focus();
},
getValue: () => value,
}));
return (
<input
ref={inputRef}
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="子组件输入框"
/>
);
});
// 父组件
function ParentForm() {
const childRef = useRef(null);
const handleReset = () => {
childRef.current.reset();
};
const handleGetValue = () => {
alert(childRef.current.getValue());
};
return (
<div>
<CustomInput ref={childRef} />
<button onClick={handleReset}>重置并聚焦</button>
<button onClick={handleGetValue}>获取子组件值</button>
</div>
);
}
7. useMemo ------ 记忆化计算结果,提升渲染性能
原理
useMemo 利用闭包 存储上一次的返回值。在渲染阶段,React 会对比依赖项数组,若依赖未变,直接返回缓存的旧值,跳过函数执行。其缓存挂载在 Fiber 节点的 memoizedState 上,属于渲染期间的计算优化。
作用
- 缓存高开销的计算(如大数据过滤、复杂数学运算)。
- 保持引用类型的稳定性(如对象、数组),避免因每次渲染重新创建而导致子组件(
React.memo)无效重绘。
语法
jsx
scss
const memoizedValue = useMemo(() => compute(a, b), [a, b]);
示例:大数据过滤与输入框丝滑交互
jsx
javascript
import { useState, useMemo } from 'react';
function FilterList() {
const [keyword, setKeyword] = useState('');
const [count, setCount] = useState(0);
// 模拟一个庞大的静态数据列表
const allItems = useMemo(() => {
const items = [];
for (let i = 0; i < 20000; i++) {
items.push(`商品-${i}`);
}
return items;
}, []); // 只在首次渲染生成一次
// 根据关键词过滤(仅当 keyword 或 allItems 变化时重新计算)
const filteredItems = useMemo(() => {
console.log('执行过滤计算...');
if (!keyword) return allItems;
return allItems.filter((item) => item.includes(keyword));
}, [keyword, allItems]);
return (
<div>
<input
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
placeholder="输入关键词过滤(体验丝滑输入)"
/>
<button onClick={() => setCount((c) => c + 1)}>重渲染计数器:{count}</button>
<ul style={{ height: '300px', overflow: 'auto', border: '1px solid #ccc' }}>
{filteredItems.slice(0, 100).map((item) => (
<li key={item}>{item}</li>
))}
{filteredItems.length > 100 && <li>...仅显示前100条</li>}
</ul>
</div>
);
}
点击计数器改变
count时,组件重渲染,但filteredItems因依赖未变直接命中缓存,列表不会重新过滤,输入框交互依然丝滑。
8. useCallback ------ 记忆化函数引用,优化子组件渲染
原理
useCallback 本质是 useMemo(() => fn, deps) 的语法糖,返回的是一个记忆化的函数。当依赖项不变时,函数的引用地址保持不变。
作用
- 当父组件重渲染时,传递给
React.memo子组件的回调函数引用不变,子组件得以跳过渲染。 - 作为
useEffect的依赖项时,避免因函数重新创建而导致 Effect 频繁执行。
语法
jsx
ini
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
示例:配合 React.memo 避免子组件无效重绘
jsx
javascript
import { useState, useCallback, memo } from 'react';
// 子组件用 memo 包裹,进行浅层 props 比较
const Child = memo(({ onAdd, label }) => {
console.log(`Child ${label} 渲染了`);
return <button onClick={onAdd}>增加 {label}</button>;
});
function Parent() {
const [countA, setCountA] = useState(0);
const [countB, setCountB] = useState(0);
const [text, setText] = useState('');
// 使用 useCallback 缓存,依赖为空,函数永远不变
const handleAddA = useCallback(() => {
setCountA((prev) => prev + 1);
}, []);
// 此处故意不使用 useCallback,每次渲染都会创建新函数
const handleAddB = () => {
setCountB((prev) => prev + 1);
};
return (
<div>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="在此输入,观察子组件渲染情况"
/>
<p>Count A: {countA}</p>
<p>Count B: {countB}</p>
<Child onAdd={handleAddA} label="A (优化)" />
<Child onAdd={handleAddB} label="B (未优化)" />
</div>
);
}
当在输入框中打字时,父组件重渲染。
Child A因handleAddA引用不变,不会重新渲染;Child B因每次接收新的handleAddB函数,导致 props 变化,触发不必要的重渲染。
扩展补充:React.memo(非 Hook,但密切配合)
React.memo 是一个高阶组件(HOC),用于缓存组件。它对 props 进行浅层比较,若 props 未变则跳过本次渲染。常与 useCallback 和 useMemo 组成性能优化的"铁三角"。
jsx
javascript
const MyComponent = memo(function MyComponent(props) {
// 仅当 props 改变时才重新渲染
});
完整总结对照表
| Hook 名称 | 核心职责 | 典型应用场景 |
|---|---|---|
useState |
管理简单局部响应式状态 | 表单输入、开关、计数器 |
useReducer |
管理含有复杂逻辑的局部状态 | 购物车、具有多种操作类型的对象状态 |
useContext |
跨组件树共享数据,避免 Props 透传 | 主题、语言、用户认证信息 |
useEffect |
执行副作用与生命周期管理 | 数据请求、DOM 操作、订阅与清理 |
useRef |
存储可变数据(不触发渲染)及 DOM 引用 | 获取 DOM 节点、保存定时器 ID、记录上一次值 |
useImperativeHandle |
自定义暴露给父组件的实例方法 | 封装表单组件、暴露 focus / reset 方法 |
useMemo |
记忆化计算结果,缓存对象/数组引用 | 大数据计算、稳定子组件 props |
useCallback |
记忆化函数引用,配合 memo 优化 |
传递给子组件的回调函数 |
进阶提示(并非全部,但值得了解)
除了上述 8 个核心 Hook,React 还提供了:
useLayoutEffect:与useEffect类似,但在 DOM 更新后、浏览器绘制前同步执行,适合处理 DOM 尺寸测量等需要避免闪烁的操作。useDebugValue:在 React DevTools 中为自定义 Hook 显示标签,方便调试。useId:生成稳定且唯一的 ID,用于 SSR 场景下的无障碍属性(如aria-describedby)。useTransition/useDeferredValue:用于并发模式下的 UI 更新降级与渲染优化。