React Hooks 完全指北(2025 版)
React Hooks 是 React 16.8 引入的革命性特性,它让函数组件拥有了状态管理和副作用处理的能力。本文将从设计哲学、核心 Hooks、高级应用、性能优化、最佳实践等多个维度,为你提供一份完整的 React Hooks 学习指南。
React Hooks 完全指北(2025 版)
摘要:React Hooks 是 React 16.8 引入的革命性特性,它让函数组件拥有了状态管理和副作用处理的能力。本文将从设计哲学、核心 Hooks、高级应用、性能优化、最佳实践等多个维度,为你提供一份完整的 React Hooks 学习指南。
目录
一、Hooks 的设计哲学
1.1 React Hooks 简介
React Hooks 是 React 16.8 版本带来的革命性特性,它让函数组件也能像类组件那样拥有状态(state)和其他 React 特性,同时还解决了类组件中的许多痛点。
类组件的问题:
- 逻辑复用困难:需要使用高阶组件(HOC)或渲染属性(Render Props),导致组件树嵌套过深
- 生命周期复杂:不相关的逻辑被强制放在同一个生命周期方法中
- this 指向问题:需要频繁使用
bind或箭头函数来绑定this - 代码组织混乱:相关逻辑分散在不同的生命周期方法中
Hooks 的优势:
- 逻辑复用:通过自定义 Hooks 实现逻辑复用,无需修改组件结构
- 代码组织:相关逻辑可以聚合在一起,而不是被生命周期方法拆分
- 函数式编程:更符合函数式编程思维,代码更简洁、易测试
- 性能优化:更细粒度的性能优化控制
1.2 Hooks 规则与最佳实践
使用 Hooks 必须遵守两条核心规则:
规则一:只在最顶层调用 Hooks
错误示例:
function MyComponent({ condition }) {
if (condition) {
const [count, setCount] = useState(0); // ❌ 违反规则
}
for (let i = 0; i < 10; i++) {
useEffect(() => {}); // ❌ 违反规则
}
}** 正确示例:**
function MyComponent({ condition }) {
const [count, setCount] = useState(0); // 在顶层调用
useEffect(() => {
if (condition) {
// 在 effect 内部使用条件逻辑
}
}, [condition]);
}原因: React 依赖 Hooks 的调用顺序来正确管理状态。如果在条件语句或循环中调用,会导致每次渲染时 Hooks 的调用顺序不一致,从而引发 bug。
规则二:只在 React 函数中调用 Hooks
** 可以调用:**
- React 函数组件
- 自定义 Hooks(以
use开头的函数)
不能调用:
- 普通 JavaScript 函数
- 类组件
- 事件处理函数(除非是自定义 Hook 的一部分)
二、核心 Hooks 详解
2.1 useState:状态管理的基础
useState 是最基础也是最重要的 Hook,用于在函数组件中声明状态。
基本用法
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>当前计数:{count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
<button onClick={() => setCount(count - 1)}>减少</button>
</div>
);
}函数式更新
当新状态依赖于旧状态时,应该使用函数式更新:
function Counter() {
const [count, setCount] = useState(0);
// ❌ 错误:可能丢失更新
const handleClick = () => {
setCount(count + 1);
setCount(count + 1); // 两次更新都基于同一个 count 值
};
// 正确:使用函数式更新
const handleClick = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1); // 每次更新都基于最新的状态
};
return <button onClick={handleClick}>点击两次增加</button>;
}惰性初始化
如果初始状态需要通过复杂计算得到,可以使用函数进行惰性初始化:
function ExpensiveComponent() {
// ❌ 错误:每次渲染都会执行 expensiveCalculation
const [value, setValue] = useState(expensiveCalculation());
// 正确:只在首次渲染时执行
const [value, setValue] = useState(() => expensiveCalculation());
}对象和数组状态
function Form() {
const [formData, setFormData] = useState({
name: "",
email: "",
age: 0
});
// 正确:使用展开运算符保持不可变性
const updateName = (name) => {
setFormData(prev => ({
...prev,
name
}));
};
// ❌ 错误:直接修改对象
const updateNameWrong = (name) => {
formData.name = name; // 不会触发重新渲染
setFormData(formData);
};
}2.2 useReducer:复杂状态管理
useReducer 是 useState 的替代方案,适用于状态逻辑复杂或包含多个子值的场景。
基本用法
import { useReducer } from "react";
// Reducer 函数:纯函数,接收当前状态和动作,返回新状态
function counterReducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return { count: 0 };
case "set":
return { count: action.payload };
default:
throw new Error(`未知动作类型: ${action.type}`);
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
<p>计数:{state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "reset" })}>重置</button>
<button onClick={() => dispatch({ type: "set", payload: 10 })}>
设置为 10
</button>
</div>
);
}惰性初始化
function init(initialCount) {
return { count: initialCount };
}
function counterReducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "reset":
return init(action.payload);
default:
return state;
}
}
function Counter({ initialCount }) {
const [state, dispatch] = useReducer(
counterReducer,
initialCount,
init // 第三个参数是初始化函数
);
return (
<>
<p>计数:{state.count}</p>
<button onClick={() => dispatch({ type: "reset", payload: initialCount })}>
重置
</button>
</>
);
}适用场景
- 状态逻辑复杂,包含多个子值
- 下一个状态依赖于前一个状态
- 需要可预测的状态更新逻辑
- 需要更好的测试性(reducer 是纯函数)
2.3 useRef:持久化引用
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。返回的 ref 对象在组件的整个生命周期内保持不变。
基本用法
import { useRef } from "react";
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current?.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>聚焦输入框</button>
</div>
);
}保存任意可变值
useRef 不仅可以保存 DOM 引用,还可以保存任何可变值,且不会触发重新渲染:
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
const startTimer = () => {
intervalRef.current = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
};
const stopTimer = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);
return (
<div>
<p>计时:{count} 秒</p>
<button onClick={startTimer}>开始</button>
<button onClick={stopTimer}>停止</button>
</div>
);
}避免闭包陷阱
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
// 保持 countRef.current 与 count 同步
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
const timer = setInterval(() => {
// 使用 ref 获取最新值,避免闭包陷阱
console.log("当前计数:", countRef.current);
}, 1000);
return () => clearInterval(timer);
}, []); // 依赖数组为空,但可以访问最新值
return <div>计数:{count}</div>;
}2.4 useEffect:副作用处理
useEffect 用于处理副作用,如数据请求、订阅、手动修改 DOM 等。
基本用法
import { useEffect, useState } from "react";
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 副作用逻辑
async function fetchUser() {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
} catch (error) {
console.error("获取用户失败:", error);
} finally {
setLoading(false);
}
}
fetchUser();
// 清理函数(可选)
return () => {
// 取消请求、清理订阅等
console.log("清理副作用");
};
}, [userId]); // 依赖数组:当 userId 变化时重新执行
if (loading) return <div>加载中...</div>;
if (!user) return <div>用户不存在</div>;
return <div>{user.name}</div>;
}依赖数组详解
useEffect(() => {
// 1. 无依赖数组:每次渲染后都执行
console.log("每次渲染都执行");
});
useEffect(() => {
// 2. 空依赖数组:仅在挂载时执行一次
console.log("仅执行一次");
}, []);
useEffect(() => {
// 3. 有依赖:依赖变化时执行
console.log("当 count 变化时执行");
}, [count]);清理副作用
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
// 清理函数:在组件卸载或依赖变化前执行
return () => {
connection.disconnect();
};
}, [roomId]);
return <div>聊天室:{roomId}</div>;
}常见副作用模式
1. 数据获取
function useFetch(url) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url);
const json = await response.json();
if (!cancelled) {
setData(json);
setError(null);
}
} catch (err) {
if (!cancelled) {
setError(err);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchData();
return () => {
cancelled = true; // 防止在组件卸载后更新状态
};
}, [url]);
return { data, error, loading };
}2. 事件监听
function WindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return <div>窗口大小:{size.width} x {size.height}</div>;
}3. 订阅
function OnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
}, []);
return <div>{isOnline ? "🟢 在线" : "🔴 离线"}</div>;
}2.5 useLayoutEffect:同步副作用
useLayoutEffect 与 useEffect 类似,但它在 DOM 更新后、浏览器绘制前同步执行。
与 useEffect 的区别
function Tooltip() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const tooltipRef = useRef(null);
// ❌ 使用 useEffect:可能闪烁
useEffect(() => {
if (tooltipRef.current) {
const rect = tooltipRef.current.getBoundingClientRect();
setPosition({ x: rect.left, y: rect.top });
}
}, []);
// 使用 useLayoutEffect:避免闪烁
useLayoutEffect(() => {
if (tooltipRef.current) {
const rect = tooltipRef.current.getBoundingClientRect();
setPosition({ x: rect.left, y: rect.top });
}
}, []);
return <div ref={tooltipRef}>工具提示</div>;
}适用场景
- 需要同步读取 DOM 布局
- 需要在浏览器绘制前更新 DOM
- 避免视觉闪烁
⚠️ 警告:useLayoutEffect 会阻塞浏览器绘制,不要滥用。
2.6 useCallback:缓存函数
useCallback 返回一个记忆化的回调函数,只有当依赖项改变时才会返回新的函数。
基本用法
import { useCallback, useState } from "react";
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState("");
// ❌ 每次渲染都创建新函数
const handleClick = () => {
console.log("点击了");
};
// 使用 useCallback 缓存函数
const handleClickMemoized = useCallback(() => {
console.log("点击了");
}, []); // 空依赖数组:函数永远不会改变
// 依赖 count:当 count 变化时创建新函数
const handleCountClick = useCallback(() => {
setCount(prev => prev + 1);
}, []); // 使用函数式更新,不需要 count 依赖
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<Child onClick={handleClickMemoized} />
</div>
);
}
// 使用 React.memo 优化子组件
const Child = React.memo(({ onClick }) => {
console.log("Child 渲染");
return <button onClick={onClick}>点击</button>;
});性能优化场景
function TodoList({ todos, onToggle }) {
// 缓存回调函数,避免子组件不必要的重新渲染
const handleToggle = useCallback((id) => {
onToggle(id);
}, [onToggle]);
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
/>
))}
</ul>
);
}2.7 useMemo:缓存计算结果
useMemo 返回一个记忆化的值,只有当依赖项改变时才会重新计算。
基本用法
import { useMemo, useState } from "react";
function ExpensiveComponent({ items, filter }) {
// ❌ 每次渲染都重新计算
const filteredItems = items.filter(item => item.category === filter);
// 使用 useMemo 缓存计算结果
const filteredItems = useMemo(() => {
return items.filter(item => item.category === filter);
}, [items, filter]);
return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}复杂计算优化
function Fibonacci({ n }) {
const result = useMemo(() => {
// 昂贵的计算
function fib(num) {
if (num <= 1) return num;
return fib(num - 1) + fib(num - 2);
}
return fib(n);
}, [n]);
return <div>第 {n} 个斐波那契数:{result}</div>;
}useMemo vs useCallback
| 特性 | useMemo | useCallback |
|---|---|---|
| 返回值 | 记忆化的值 | 记忆化的函数 |
| 使用场景 | 昂贵计算 | 函数引用稳定 |
| 示例 | const value = useMemo(() => compute(a, b), [a, b]) | const fn = useCallback(() => {}, []) |
2.8 useContext:上下文共享
useContext 用于在组件树中共享数据,避免 props 层层传递。
基本用法
import { createContext, useContext } from "react";
// 1. 创建 Context
const ThemeContext = createContext("light");
// 2. 提供 Context 值
function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
}
// 3. 消费 Context 值
function Button() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
className={theme}
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
>
当前主题:{theme}
</button>
);
}性能优化
// 拆分 Context,避免不必要的重新渲染
const ThemeContext = createContext("light");
const SetThemeContext = createContext(() => {});
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={theme}>
<SetThemeContext.Provider value={setTheme}>
{children}
</SetThemeContext.Provider>
</ThemeContext.Provider>
);
}
// 只有需要 theme 的组件会重新渲染
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>按钮</button>;
}
// 只有需要 setTheme 的组件会重新渲染
function ThemeToggle() {
const setTheme = useContext(SetThemeContext);
return <button onClick={() => setTheme("dark")}>切换主题</button>;
}三、高级 Hooks 与应用
3.1 useImperativeHandle:自定义 Ref API
useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。
import { forwardRef, useImperativeHandle, useRef } from "react";
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
scrollIntoView: () => {
inputRef.current.scrollIntoView();
},
getValue: () => {
return inputRef.current.value;
}
}), []);
return <input ref={inputRef} {...props} />;
});
function Parent() {
const inputRef = useRef();
return (
<div>
<FancyInput ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>聚焦</button>
<button onClick={() => console.log(inputRef.current?.getValue())}>
获取值
</button>
</div>
);
}3.2 useInsertionEffect:样式插入(React 18+)
useInsertionEffect 在 DOM 更新前同步执行,主要用于注入样式。
import { useInsertionEffect } from "react";
function useCSS(rule) {
useInsertionEffect(() => {
const style = document.createElement("style");
style.textContent = rule;
document.head.appendChild(style);
return () => {
document.head.removeChild(style);
};
}, [rule]);
}
function StyledComponent() {
useCSS(`
.my-class {
color: red;
}
`);
return <div className="my-class">红色文字</div>;
}3.3 useSyncExternalStore:订阅外部 Store(React 18+)
useSyncExternalStore 用于订阅外部数据源,确保与并发渲染兼容。
import { useSyncExternalStore } from "react";
// 简单的 Store 实现
function createStore(initialState) {
let state = initialState;
const listeners = new Set();
return {
getState: () => state,
setState: (newState) => {
state = newState;
listeners.forEach(listener => listener());
},
subscribe: (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
}
};
}
const store = createStore({ count: 0 });
function Counter() {
const state = useSyncExternalStore(
store.subscribe,
store.getState
);
return (
<div>
<p>计数:{state.count}</p>
<button onClick={() => store.setState({ count: state.count + 1 })}>
增加
</button>
</div>
);
}3.4 useDeferredValue:延迟值(React 18+)
useDeferredValue 可以延迟更新某个值,让 UI 保持响应。
import { useDeferredValue, useMemo } from "react";
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => {
// 昂贵的搜索操作
return expensiveSearch(deferredQuery);
}, [deferredQuery]);
return (
<div>
{query !== deferredQuery && <div>搜索中...</div>}
<ResultsList results={results} />
</div>
);
}3.5 useTransition:并发更新(React 18+)
useTransition 可以标记非紧急更新,提升交互响应速度。
import { useTransition, useState } from "react";
function SearchBox() {
const [isPending, startTransition] = useTransition();
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const handleChange = (e) => {
const value = e.target.value;
setQuery(value); // 紧急更新:立即更新输入框
startTransition(() => {
// 非紧急更新:延迟更新搜索结果
setResults(search(value));
});
};
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <div>搜索中...</div>}
<ResultsList results={results} />
</div>
);
}3.6 useActionState:异步提交状态(React 19+)
useActionState 用于管理异步提交的状态,特别适用于表单提交。
import { useActionState } from "react";
async function submitForm(prevState, formData) {
try {
const response = await fetch("/api/submit", {
method: "POST",
body: formData
});
if (!response.ok) {
return { error: "提交失败" };
}
return { success: true, message: "提交成功!" };
} catch (error) {
return { error: error.message };
}
}
function ContactForm() {
const [state, formAction, isPending] = useActionState(submitForm, null);
return (
<form action={formAction}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit" disabled={isPending}>
{isPending ? "提交中..." : "提交"}
</button>
{state?.error && <div className="error">{state.error}</div>}
{state?.success && <div className="success">{state.message}</div>}
</form>
);
}3.7 useOptimistic:乐观更新(React 19+)
useOptimistic 用于实现乐观更新,在服务器响应前先更新 UI。
import { useOptimistic, useTransition } from "react";
function CommentList({ comments: initialComments }) {
const [comments, setOptimisticComments] = useOptimistic(
initialComments,
(state, newComment) => [...state, newComment]
);
const [isPending, startTransition] = useTransition();
async function addComment(newComment) {
// 乐观更新:立即显示新评论
startTransition(() => {
setOptimisticComments(newComment);
});
try {
// 实际提交到服务器
await postComment(newComment);
} catch (error) {
// 如果失败,可以回滚
console.error("提交失败:", error);
}
}
return (
<div>
{comments.map(comment => (
<div key={comment.id}>{comment.text}</div>
))}
{isPending && <div>添加中...</div>}
</div>
);
}3.8 useEffectEvent:稳定副作用事件(React 19+)
useEffectEvent 解决了 useEffect 中的闭包陷阱问题。
import { useEffectEvent } from "react";
function ChatRoom({ roomId, userId }) {
const onMessage = useEffectEvent((message) => {
// 这个函数可以访问最新的 props,但不会导致 effect 重新执行
sendMessage(roomId, userId, message);
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on("message", onMessage);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]); // 不需要将 onMessage 或 userId 加入依赖
return <div>聊天室:{roomId}</div>;
}3.9 useDebugValue:调试辅助
useDebugValue 用于在 React DevTools 中显示自定义 Hook 的标签。
import { useDebugValue, useState, useEffect } from "react";
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
}, []);
// 在 React DevTools 中显示调试值
useDebugValue(isOnline ? "🟢 在线" : "🔴 离线");
return isOnline;
}四、自定义 Hooks 实战
自定义 Hooks 是以 use 开头的函数,可以组合多个内置 Hooks 来实现逻辑复用。
4.1 数据获取 Hook
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
const controller = new AbortController();
async function fetchData() {
try {
setLoading(true);
setError(null);
const response = await fetch(url, {
...options,
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
if (!cancelled) {
setData(json);
}
} catch (err) {
if (!cancelled && err.name !== "AbortError") {
setError(err);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchData();
return () => {
cancelled = true;
controller.abort();
};
}, [url, JSON.stringify(options)]);
return { data, error, loading, refetch: () => fetchData() };
}4.2 本地存储 Hook
function useLocalStorage(key, initialValue) {
// 从 localStorage 读取初始值
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;
}
});
// 更新状态和 localStorage
const setValue = useCallback((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);
}
}, [key, storedValue]);
return [storedValue, setValue];
}4.3 防抖 Hook
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// 使用示例
function SearchInput() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 500);
useEffect(() => {
if (debouncedQuery) {
// 执行搜索
search(debouncedQuery);
}
}, [debouncedQuery]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}4.4 节流 Hook
function useThrottle(value, limit) {
const [throttledValue, setThrottledValue] = useState(value);
const lastRan = useRef(Date.now());
useEffect(() => {
const handler = setTimeout(() => {
if (Date.now() - lastRan.current >= limit) {
setThrottledValue(value);
lastRan.current = Date.now();
}
}, limit - (Date.now() - lastRan.current));
return () => {
clearTimeout(handler);
};
}, [value, limit]);
return throttledValue;
}4.5 窗口尺寸 Hook
function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: typeof window !== "undefined" ? window.innerWidth : 0,
height: typeof window !== "undefined" ? window.innerHeight : 0
});
useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
}
window.addEventListener("resize", handleResize);
handleResize(); // 立即调用一次
return () => window.removeEventListener("resize", handleResize);
}, []);
return windowSize;
}4.6 点击外部 Hook
function useClickOutside(ref, handler) {
useEffect(() => {
function handleClickOutside(event) {
if (ref.current && !ref.current.contains(event.target)) {
handler(event);
}
}
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("touchstart", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("touchstart", handleClickOutside);
};
}, [ref, handler]);
}
// 使用示例
function Dropdown() {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
useClickOutside(dropdownRef, () => setIsOpen(false));
return (
<div ref={dropdownRef}>
<button onClick={() => setIsOpen(!isOpen)}>打开菜单</button>
{isOpen && <div>下拉内容</div>}
</div>
);
}4.7 媒体查询 Hook
function useMediaQuery(query) {
const [matches, setMatches] = useState(() => {
if (typeof window !== "undefined") {
return window.matchMedia(query).matches;
}
return false;
});
useEffect(() => {
const mediaQuery = window.matchMedia(query);
const handleChange = (event) => {
setMatches(event.matches);
};
// 使用 addEventListener(现代浏览器)
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
} else {
// 兼容旧浏览器
mediaQuery.addListener(handleChange);
return () => mediaQuery.removeListener(handleChange);
}
}, [query]);
return matches;
}
// 使用示例
function ResponsiveComponent() {
const isMobile = useMediaQuery("(max-width: 768px)");
const isTablet = useMediaQuery("(min-width: 769px) and (max-width: 1024px)");
const isDesktop = useMediaQuery("(min-width: 1025px)");
if (isMobile) return <div>移动端布局</div>;
if (isTablet) return <div>平板布局</div>;
if (isDesktop) return <div>桌面布局</div>;
return null;
}4.8 异步操作 Hook
function useAsync(asyncFunction, immediate = true) {
const [status, setStatus] = useState("idle");
const [value, setValue] = useState(null);
const [error, setError] = useState(null);
const execute = useCallback(async (...args) => {
setStatus("pending");
setValue(null);
setError(null);
try {
const response = await asyncFunction(...args);
setValue(response);
setStatus("success");
return response;
} catch (err) {
setError(err);
setStatus("error");
throw err;
}
}, [asyncFunction]);
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return { execute, status, value, error, loading: status === "pending" };
}
// 使用示例
function UserProfile({ userId }) {
const { value: user, loading, error, execute: refetch } = useAsync(
() => fetchUser(userId),
true
);
if (loading) return <div>加载中...</div>;
if (error) return <div>错误:{error.message}</div>;
if (!user) return null;
return (
<div>
<h1>{user.name}</h1>
<button onClick={refetch}>刷新</button>
</div>
);
}五、性能优化策略
5.1 避免不必要的重新渲染
// ❌ 错误:每次渲染都创建新对象
function Parent() {
const data = { name: "John" };
return <Child data={data} />;
}
// 正确:使用 useMemo
function Parent() {
const data = useMemo(() => ({ name: "John" }), []);
return <Child data={data} />;
}
// 或使用 React.memo
const Child = React.memo(({ data }) => {
return <div>{data.name}</div>;
});5.2 正确使用依赖数组
function Component({ userId }) {
const [user, setUser] = useState(null);
// ❌ 错误:缺少依赖
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // 缺少 userId 依赖
// 正确:包含所有依赖
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
// 或使用 useCallback 稳定函数引用
const fetchUserData = useCallback(async () => {
const data = await fetchUser(userId);
setUser(data);
}, [userId]);
useEffect(() => {
fetchUserData();
}, [fetchUserData]);
}5.3 拆分大组件
// ❌ 错误:一个巨大的组件
function HugeComponent() {
const [state1, setState1] = useState();
const [state2, setState2] = useState();
// ... 很多状态和逻辑
return (
<div>
{/* 大量 JSX */}
</div>
);
}
// 正确:拆分为多个小组件
function ParentComponent() {
return (
<div>
<HeaderSection />
<ContentSection />
<FooterSection />
</div>
);
}
function HeaderSection() {
const [state, setState] = useState();
// 只管理自己的状态
return <header>...</header>;
}5.4 使用 React.memo 优化子组件
const ExpensiveChild = React.memo(({ data, onUpdate }) => {
console.log("ExpensiveChild 渲染");
return (
<div>
<p>{data.name}</p>
<button onClick={onUpdate}>更新</button>
</div>
);
}, (prevProps, nextProps) => {
// 自定义比较函数
return prevProps.data.id === nextProps.data.id &&
prevProps.data.name === nextProps.data.name;
});
function Parent() {
const [count, setCount] = useState(0);
const [user, setUser] = useState({ id: 1, name: "John" });
const handleUpdate = useCallback(() => {
setUser(prev => ({ ...prev, name: "Jane" }));
}, []);
return (
<div>
<p>计数:{count}</p>
<button onClick={() => setCount(c => c + 1)}>增加计数</button>
<ExpensiveChild data={user} onUpdate={handleUpdate} />
</div>
);
}六、常见陷阱与解决方案
6.1 闭包陷阱
// ❌ 错误:闭包陷阱
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 总是打印初始值 0
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖数组
return <div>{count}</div>;
}
// 解决方案1:使用函数式更新
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1); // 使用函数式更新
}, 1000);
return () => clearInterval(timer);
}, []);
return <div>{count}</div>;
}
// 解决方案2:使用 useRef
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
const timer = setInterval(() => {
console.log(countRef.current); // 总是最新值
setCount(countRef.current + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
return <div>{count}</div>;
}
// 解决方案3:使用 useEffectEvent (React 19+)
function Counter() {
const [count, setCount] = useState(0);
const logCount = useEffectEvent(() => {
console.log(count); // 总是最新值
});
useEffect(() => {
const timer = setInterval(() => {
logCount();
setCount(prev => prev + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
return <div>{count}</div>;
}6.2 无限循环
// ❌ 错误:无限循环
function Component() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // 每次渲染都更新,导致无限循环
}); // 缺少依赖数组
return <div>{count}</div>;
}
// 正确:添加依赖数组
function Component() {
const [count, setCount] = useState(0);
useEffect(() => {
// 只在特定条件下更新
if (count < 10) {
setCount(count + 1);
}
}, [count]); // 明确依赖
return <div>{count}</div>;
}6.3 对象/数组依赖问题
// ❌ 错误:对象引用总是变化
function Component() {
const [data, setData] = useState({ count: 0 });
useEffect(() => {
console.log(data);
}, [data]); // data 对象引用每次都不同,导致无限循环
return <div>{data.count}</div>;
}
// 解决方案1:使用具体属性
function Component() {
const [data, setData] = useState({ count: 0 });
useEffect(() => {
console.log(data.count);
}, [data.count]); // 只依赖 count 属性
return <div>{data.count}</div>;
}
// 解决方案2:使用 useMemo 稳定引用
function Component() {
const [data, setData] = useState({ count: 0 });
const stableData = useMemo(() => data, [data.count]);
useEffect(() => {
console.log(stableData);
}, [stableData]);
return <div>{data.count}</div>;
}6.4 清理函数忘记调用
// ❌ 错误:忘记清理
function Component() {
useEffect(() => {
const subscription = subscribe();
// 忘记取消订阅
}, []);
}
// 正确:总是清理副作用
function Component() {
useEffect(() => {
const subscription = subscribe();
return () => {
subscription.unsubscribe(); // 清理订阅
};
}, []);
}七、最佳实践
7.1 自定义 Hooks 命名规范
// 正确:以 use 开头
function useCounter() {}
function useFetch() {}
function useLocalStorage() {}
// ❌ 错误:不以 use 开头
function getCounter() {}
function fetchData() {}7.2 单一职责原则
// ❌ 错误:一个 Hook 做太多事情
function useUserData() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [comments, setComments] = useState([]);
// ... 太多职责
}
// 正确:拆分为多个 Hooks
function useUser(userId) {
// 只管理用户数据
}
function useUserPosts(userId) {
// 只管理用户文章
}
function useUserComments(userId) {
// 只管理用户评论
}7.3 错误处理
function useAsyncOperation(operation) {
const [state, setState] = useState({
data: null,
loading: false,
error: null
});
const execute = useCallback(async (...args) => {
setState(prev => ({ ...prev, loading: true, error: null }));
try {
const data = await operation(...args);
setState({ data, loading: false, error: null });
return data;
} catch (error) {
setState(prev => ({
...prev,
loading: false,
error: error.message || "操作失败"
}));
throw error;
}
}, [operation]);
return { ...state, execute };
}7.4 类型安全(TypeScript)
import { useState, useEffect } from "react";
interface UseFetchResult<T> {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}
function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
const response = await fetch(url);
const json = await response.json();
setData(json);
} catch (err) {
setError(err instanceof Error ? err : new Error("未知错误"));
} finally {
setLoading(false);
}
}, [url]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch: fetchData };
}八、总结
8.1 Hooks 核心要点
- 状态管理:
useState用于简单状态,useReducer用于复杂状态 - 副作用处理:
useEffect用于异步副作用,useLayoutEffect用于同步副作用 - 性能优化:
useMemo缓存计算,useCallback缓存函数 - 引用管理:
useRef保存可变值,不触发重新渲染 - 上下文共享:
useContext避免 props 层层传递
8.2 版本特性总结
| React 版本 | 新增 Hooks |
|---|---|
| 16.8 | useState, useEffect, useContext, useReducer, useCallback, useMemo, useRef, useImperativeHandle, useLayoutEffect, useDebugValue |
| 18.0 | useId, useTransition, useDeferredValue, useSyncExternalStore, useInsertionEffect |
| 19.0 | useActionState, useOptimistic, useEffectEvent, useFormStatus, useFormState |
8.3 学习路径建议
- 基础阶段:掌握
useState、useEffect、useContext - 进阶阶段:学习
useReducer、useCallback、useMemo、自定义 Hooks - 高级阶段:理解 React 18+ 的并发特性,掌握
useTransition、useDeferredValue等 - 实战阶段:在实际项目中应用,解决复杂业务场景
8.4 记忆口诀
Hooks 三原则:顶层调用、React 函数、依赖明确
- 只在最顶层调用 Hooks
- 只在 React 函数中调用 Hooks
- 明确依赖数组,避免闭包陷阱
标签:React, Hooks, 前端开发, JavaScript, 函数式编程, React 18, React 19
周温
全栈开发工程师,专注于前端技术栈和物联网应用开发。拥有丰富的 React、Next.js、Nest.js 项目经验,擅长构建高性能、可扩展的 Web 应用。热爱技术分享,致力于将复杂的技术问题转化为易懂的实践指南。