React Hooks 完全指南:从内置 Hook 到自定义 Hook,一文彻底掌握
React 16.8 引入了 Hooks,从此函数组件拥有了类组件的一切能力。本文将带你系统学习所有内置 Hooks 的用法,并教你如何封装属于自己的自定义 Hook,让代码复用变得轻而易举。
前言:为什么需要 Hooks?
在 React 16.8 之前,函数组件只能接收 props 并渲染 UI,无法管理状态、无法处理副作用。如果你需要这些能力,就必须写成类组件。
Hooks 的出现彻底改变了这一局面。它让开发者在不编写类组件的情况下使用 React 的状态和其他特性,极大地扩展了函数组件的能力。从此,函数式编程范式在 React 中得到了更广泛的应用。
Hooks 的核心价值
- 逻辑复用:将组件逻辑抽离成可复用的函数,而不仅仅是 UI 复用
- 代码清晰:将复杂组件拆分成多个小函数,每个函数专注一件事
- 性能优化 :通过
useMemo、useCallback等 Hook 避免不必要的计算和渲染
一、Hooks 的黄金规则
Hooks 看起来是普通的 JavaScript 函数,但它们代表了一种特殊类型的可复用 UI 逻辑,对调用位置有着严格限制。
规则一:只在顶层调用 Hooks
不要 在循环、条件判断、嵌套函数或 try/catch/finally 块中调用 Hooks。始终 在 React 函数组件的顶层、任何 return 之前调用 Hooks。
jsx
// ❌ 错误:在条件语句中调用
function Bad({ cond }) {
if (cond) {
const theme = useContext(ThemeContext); // 禁止!
}
}
// ❌ 错误:在循环中调用
function Bad() {
for (let i = 0; i < 10; i++) {
const theme = useContext(ThemeContext); // 禁止!
}
}
// ❌ 错误:在条件 return 之后调用
function Bad({ cond }) {
if (cond) return;
const theme = useContext(ThemeContext); // 禁止!
}
// ❌ 错误:在事件处理函数中调用
function Bad() {
function handleClick() {
const theme = useContext(ThemeContext); // 禁止!
}
}
// ❌ 错误:在类组件中调用
class Bad extends React.Component {
render() {
useEffect(() => {}); // 禁止!
}
}
// ✅ 正确:在函数组件顶层调用
function Good() {
const [count, setCount] = useState(0); // ✅
const theme = useContext(ThemeContext); // ✅
// ...
}
为什么? React 依赖 Hooks 的调用顺序来正确保存每个 Hook 的状态。如果 Hooks 被条件性地调用,顺序就会被打乱,导致状态错乱。
规则二:只在 React 函数中调用 Hooks
- ✅ 在 React 函数组件中调用
- ✅ 在自定义 Hook 中调用
- ❌ 不要在普通 JavaScript 函数中调用
jsx
// ❌ 错误:在普通 JS 函数中调用
function normalFunction() {
const [count, setCount] = useState(0); // 禁止!
}
// ✅ 正确:在自定义 Hook 中调用
function useCustomHook() {
const [count, setCount] = useState(0); // ✅ 自定义 Hook 可以调用其他 Hook
return count;
}
补充:自定义 Hook 可以调用其他 Hook,这正是它们存在的全部意义。因为自定义 Hook 也只在函数组件渲染时被调用。
二、内置 Hooks 分类全景
React 内置了十余个 Hooks,按功能可分为以下几大类:
| 分类 | Hooks | 用途 |
|---|---|---|
| State Hooks | useState、useReducer |
管理组件状态 |
| Context Hooks | useContext |
读取订阅上下文 |
| Ref Hooks | useRef、useImperativeHandle |
保存不用于渲染的信息 |
| Effect Hooks | useEffect、useLayoutEffect、useInsertionEffect |
连接外部系统 |
| Performance Hooks | useMemo、useCallback |
跳过计算和不必要的重渲染 |
| Other Hooks | useTransition、useDeferredValue、useId、useSyncExternalStore、useDebugValue |
并发渲染、调试等特殊场景 |
三、State Hooks:状态管理
1. useState ------ 最基础的状态 Hook
useState 允许在函数组件中添加状态。它返回一个状态值和更新该状态值的函数。
jsx
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // 初始值为 0
return (
<div>
<p>点击了 {count} 次</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
惰性初始化:如果初始状态需要复杂计算,可以传入函数:
jsx
const [state, setState] = useState(() => {
const expensiveResult = someExpensiveComputation();
return expensiveResult;
});
2. useReducer ------ 复杂状态逻辑
当状态更新逻辑复杂、涉及多个子值或依赖先前状态时,useReducer 是比 useState 更好的选择。
jsx
import { useReducer } from 'react';
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 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</>
);
}
四、Context Hook:跨层级传递数据
useContext ------ 摆脱 props 层层传递
useContext 让组件从祖先组件接收信息,而无需将其作为 props 逐层传递。
jsx
import { useContext, createContext } from 'react';
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
return <Button />;
}
function Button() {
const theme = useContext(ThemeContext); // 直接读取,无需 props 传递
return <button className={theme}>按钮</button>;
}
五、Ref Hooks:保存不用于渲染的数据
1. useRef ------ 不触发渲染的"存储箱"
Ref 允许组件保存一些不用于渲染的信息,比如 DOM 节点或 timeout ID。与 state 不同,更新 ref 不会触发组件重新渲染。
jsx
import { useRef, useEffect } from 'react';
function TextInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // 组件挂载后自动聚焦
}, []);
return <input ref={inputRef} type="text" />;
}
2. useImperativeHandle ------ 自定义暴露给父组件的 ref
useImperativeHandle 允许你在使用 ref 时自定义暴露给父组件的实例值或方法。这在你希望限制父组件对子组件的访问权限时非常有用。
jsx
import { forwardRef, useImperativeHandle, useRef } from 'react';
const ChildInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
// 只暴露 focus 方法给父组件,不暴露整个 DOM 节点
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
clear: () => {
inputRef.current.value = '';
}
}));
return <input ref={inputRef} />;
});
function Parent() {
const childRef = useRef(null);
return (
<>
<ChildInput ref={childRef} />
<button onClick={() => childRef.current.focus()}>聚焦子输入框</button>
</>
);
}
六、Effect Hooks:连接外部系统
1. useEffect ------ 最常用的副作用 Hook
useEffect 允许组件连接到外部系统并与之同步。这包括数据获取、订阅管理、DOM 操作等。
jsx
import { useState, useEffect } from 'react';
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
// 副作用:连接聊天室
const connection = createConnection(roomId);
connection.connect();
connection.on('message', (msg) => {
setMessages(prev => [...prev, msg]);
});
// 清理函数:组件卸载或 roomId 变化时断开连接
return () => {
connection.disconnect();
};
}, [roomId]); // 依赖数组:roomId 变化时重新执行
return <div>{/* 渲染消息列表 */}</div>;
}
依赖数组的三种情况:
useEffect(() => {}, [])--- 只在组件挂载时执行一次useEffect(() => {}, [a, b])--- 在 a 或 b 变化时执行useEffect(() => {})--- 每次渲染后都执行(慎用!)
重要提醒 :Effect 是 React 范式中的"脱围机制"。如果不需要与外部系统交互,那么你可能不需要 Effect。
2. useLayoutEffect ------ 在浏览器绘制前同步执行
useLayoutEffect 与 useEffect 类似,但它在浏览器重新绘制屏幕之前同步执行。适合测量布局、防止闪烁等场景。
jsx
import { useLayoutEffect, useRef, useState } from 'react';
function Tooltip() {
const ref = useRef(null);
const [height, setHeight] = useState(0);
// 在浏览器绘制前测量 DOM 高度,避免闪烁
useLayoutEffect(() => {
if (ref.current) {
setHeight(ref.current.getBoundingClientRect().height);
}
}, []);
return <div ref={ref}>高度是:{height}px</div>;
}
useEffectvsuseLayoutEffect:useEffect是异步执行的,适合绝大多数场景;useLayoutEffect是同步执行的,适合需要在布局更新后立即执行的操作。
七、Performance Hooks:性能优化
1. useMemo ------ 缓存计算结果
useMemo 返回一个记忆化的值,避免在每次渲染时重复计算。
jsx
import { useMemo, useState } from 'react';
function ExpensiveComponent({ a, b }) {
const [count, setCount] = useState(0);
// 只有 a 或 b 变化时才重新计算,否则直接使用缓存值
const expensiveResult = useMemo(() => {
console.log('执行昂贵计算...');
return a * b * 1000; // 模拟复杂计算
}, [a, b]);
return (
<div>
结果:{expensiveResult}
<button onClick={() => setCount(count + 1)}>无关状态:{count}</button>
</div>
);
}
2. useCallback ------ 缓存函数引用
useCallback 返回一个记忆化的回调函数,防止不必要的渲染。
jsx
import { useCallback, useState } from 'react';
function Parent() {
const [count, setCount] = useState(0);
// 只有依赖变化时才创建新函数,否则复用之前的
const handleClick = useCallback(() => {
console.log('点击了');
}, []); // 空依赖 → 永远不变
return (
<>
<Child onClick={handleClick} />
<button onClick={() => setCount(count + 1)}>父组件状态:{count}</button>
</>
);
}
useMemovsuseCallback:useMemo缓存值 ,useCallback缓存函数。两者都用于性能优化,但不要过度使用------只有在确实存在性能问题时才使用。
八、React 18 新增 Hooks
React 18 引入了多个新 Hook,主要围绕并发渲染 和服务端渲染两大主题。
1. useTransition ------ 标记非紧急更新
useTransition 让你将某些状态更新标记为不紧急。默认情况下其他状态更新被视为紧急的,React 允许紧急更新(如输入框输入)中断非紧急更新(如渲染搜索结果列表)。
jsx
import { useState, useTransition } from 'react';
function SearchPage() {
const [input, setInput] = useState('');
const [list, setList] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
setInput(value); // 紧急更新:立即更新输入框
startTransition(() => {
// 非紧急更新:可以被打断
const newList = generateLargeList(value);
setList(newList);
});
};
return (
<div>
<input value={input} onChange={handleChange} />
{isPending && <span>加载中...</span>}
<ul>{list.map(item => <li key={item.id}>{item.name}</li>)}</ul>
</div>
);
}
2. useDeferredValue ------ 延迟非紧急值
useDeferredValue 让你推迟重新渲染树的非紧急部分。它类似于防抖,但没有固定的时间延迟------React 会在第一次渲染完成后立即尝试延迟渲染。
jsx
import { useState, useDeferredValue, useMemo } from 'react';
function SearchPage() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
// 只有 deferredText 变化时才重新计算,且计算可被中断
const list = useMemo(() => {
return generateLargeList(deferredText);
}, [deferredText]);
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<List items={list} />
</div>
);
}
3. useId ------ 生成唯一 ID
useId 用于在客户端和服务器上生成唯一 ID,同时避免水合不匹配。它主要用于与需要唯一 ID 的可访问性 API 集成的组件库。
jsx
import { useId } from 'react';
function FormField({ label }) {
const id = useId(); // 每次调用生成唯一 ID
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type="text" />
</div>
);
}
九、自定义 Hook:封装可复用逻辑
自定义 Hook 允许你将组件逻辑提取到可复用的函数中。它是 React 实现逻辑复用的核心机制。
核心规则
- Hook 名称必须以
use开头,然后紧跟大写字母 - 自定义 Hook 可以调用其他 Hook(内置 Hook 或其他自定义 Hook)
- 自定义 Hook 应该只在函数组件渲染时被调用
示例一:useOnlineStatus ------ 监听网络状态
假设你需要在多个组件中追踪用户是否在线:
jsx
// useOnlineStatus.js
import { useState, useEffect } from 'react';
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
// 使用
function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? '✅ 在线' : '❌ 离线'}</h1>;
}
function SaveButton() {
const isOnline = useOnlineStatus();
return (
<button disabled={!isOnline}>
{isOnline ? '保存' : '重连中...'}
</button>
);
}
示例二:useLocalStorage ------ 与 localStorage 同步
jsx
// useLocalStorage.js
import { useState } from 'react';
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error('读取 localStorage 失败:', error);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error('写入 localStorage 失败:', error);
}
};
return [storedValue, setValue];
}
// 使用
function UserPreferences() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [language, setLanguage] = useLocalStorage('language', 'zh');
return (
<div>
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="light">亮色</option>
<option value="dark">暗色</option>
</select>
<select value={language} onChange={(e) => setLanguage(e.target.value)}>
<option value="zh">中文</option>
<option value="en">英文</option>
</select>
</div>
);
}
示例三:useDebounce ------ 防抖
jsx
// useDebounce.js
import { useState, useEffect } from 'react';
function useDebounce(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}
// 使用
function SearchInput() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearch = useDebounce(searchTerm, 500);
useEffect(() => {
if (debouncedSearch) {
// 只有用户停止输入 500ms 后才会发起搜索请求
fetchSearchResults(debouncedSearch);
}
}, [debouncedSearch]);
return (
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="搜索..."
/>
);
}
自定义 Hook 的最佳实践
- 保持聚焦:每个自定义 Hook 应该有单一、具体的用途
- 避免不必要的依赖:注意依赖数组,防止无限循环
- 让调用代码更声明式:好的自定义 Hook 通过约束其行为让调用代码更清晰
- 返回你需要的数据:Hook 可以返回任何值------数组、对象、单个值或函数
- 按领域或功能组织:将相关的 Hook 放在一起
十、完整 Hook 速查表
| Hook | 分类 | 用途 | 关键点 |
|---|---|---|---|
useState |
State | 管理简单状态 | 返回 [state, setState] |
useReducer |
State | 管理复杂状态逻辑 | 适合多个子值或复杂更新 |
useContext |
Context | 读取上下文 | 替代 props 层层传递 |
useRef |
Ref | 保存不触发渲染的值 | .current 属性可变 |
useImperativeHandle |
Ref | 自定义暴露的 ref | 配合 forwardRef 使用 |
useEffect |
Effect | 处理副作用 | 依赖数组控制执行时机 |
useLayoutEffect |
Effect | 同步执行副作用 | 在浏览器绘制前执行 |
useMemo |
Performance | 缓存计算结果 | 避免重复计算 |
useCallback |
Performance | 缓存函数引用 | 避免子组件不必要的重渲染 |
useTransition |
Other | 标记非紧急更新 | React 18 新特性 |
useDeferredValue |
Other | 延迟非紧急值 | React 18 新特性 |
useId |
Other | 生成唯一 ID | React 18 新特性 |
useDebugValue |
Other | 调试自定义 Hook | 仅在 React DevTools 中显示 |
结语
React Hooks 是 React 生态中最重要的革新之一。从 useState 到 useEffect,从 useMemo 到自定义 Hook,这套 API 设计让函数组件变得无比强大,同时保持了代码的简洁和可维护性。
记住三条核心原则:
- 只在顶层调用 Hooks------不要放在循环、条件或嵌套函数中
- 只在 React 函数中调用 Hooks------函数组件或自定义 Hook
- 自定义 Hook 必须以
use开头
当你发现自己在多个组件中重复相同的逻辑时,就是时候提取一个自定义 Hook了。这正是 Hooks 设计的初衷------让逻辑复用变得简单而优雅。
如果觉得有用,欢迎点赞、收藏、转发,让更多小伙伴受益!我们下期再见 👋