进阶Hooks与自定义Hook —— 构建可复用的逻辑积木
上一章我们掌握了 useState、useEffect 和 useRef 这三个最核心的 Hook。但随着应用变得复杂,你会发现一些新的困惑:state 的修改逻辑越来越长怎么办?父组件的数据要穿过五层子组件才能到达目标怎么办?每次父组件一更新,所有子组件都在做无用功怎么办?好几个组件里写了一模一样的逻辑怎么办?——这些问题,正是进阶 Hook 和自定义 Hook 要解决的。它们就像一套精密的”逻辑积木”,让你把散落各处的重复代码拼装成一个个标准化、可插拔的模块。
📋 开篇自测:
- 当一个表单有十几个字段,修改逻辑又各不相同时,你会选择写十几个
useState,还是有更好的方案?- 你能说出
useMemo和useCallback的区别,以及它们各自必须搭配什么才能真正起到优化作用吗?- 如果让你封装一个
useLocalStorageHook,你会怎么设计它的入参和返回值?
一、useReducer —— 复杂状态的”指挥官”
1.1 为什么 useState 不够用了
假设你正在做一个购物车。购物车里的商品可以增加、减少、删除、清空,还可以批量修改数量。如果用 useState,你可能要在每个按钮的 onClick 里写一大段修改数组的逻辑:
// 用 useState 管理购物车 —— 逻辑散落在各处
const [cart, setCart] = useState([]);
// 添加商品
const handleAdd = (product) => {
setCart(prev => {
const existing = prev.find(item => item.id === product.id);
if (existing) {
return prev.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
return [...prev, { ...product, quantity: 1 }];
});
};
// 减少数量
const handleDecrease = (id) => {
setCart(prev => prev.map(item =>
item.id === id ? { ...item, quantity: Math.max(0, item.quantity - 1) } : item
).filter(item => item.quantity > 0));
};
// 清空
const handleClear = () => setCart([]);
这段代码能跑,但有三个问题:修改逻辑散落在不同的事件处理函数里,难以复用;如果另一个组件也需要操作购物车,你得把逻辑再写一遍;测试很困难,因为逻辑和组件绑死了。
这就是 useReducer 上场的时机。打个比方:如果 useState 是你亲自动手改数据,那 useReducer 就是你雇了一个”指挥官”(reducer 函数),你只需要下达命令(dispatch action),指挥官自己知道怎么处理。
1.2 useReducer 基础用法
import { useReducer } from 'react';
// 第一步:定义 reducer —— 所有修改逻辑集中在此
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM': {
const existing = state.find(item => item.id === action.product.id);
if (existing) {
return state.map(item =>
item.id === action.product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
return [...state, { ...action.product, quantity: 1 }];
}
case 'DECREASE_ITEM':
return state
.map(item =>
item.id === action.id
? { ...item, quantity: item.quantity - 1 }
: item
)
.filter(item => item.quantity > 0);
case 'REMOVE_ITEM':
return state.filter(item => item.id !== action.id);
case 'CLEAR':
return [];
default:
return state;
}
}
// 第二步:在组件中使用
function ShoppingCart() {
const [cart, dispatch] = useReducer(cartReducer, []);
const products = [
{ id: 1, name: 'React实战指南', price: 59 },
{ id: 2, name: 'TypeScript手册', price: 49 },
{ id: 3, name: '前端工程化', price: 69 },
];
const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
return (
<div>
<h2>书店</h2>
{products.map(p => (
<div key={p.id}>
{p.name} - ¥{p.price}
<button onClick={() => dispatch({ type: 'ADD_ITEM', product: p })}>
加入购物车
</button>
</div>
))}
<h2>购物车</h2>
{cart.length === 0 && <p>空空如也</p>}
{cart.map(item => (
<div key={item.id}>
{item.name} × {item.quantity}
<button onClick={() => dispatch({ type: 'DECREASE_ITEM', id: item.id })}>-</button>
<button onClick={() => dispatch({ type: 'REMOVE_ITEM', id: item.id })}>删除</button>
</div>
))}
{cart.length > 0 && (
<>
<p>总计:¥{total}</p>
<button onClick={() => dispatch({ type: 'CLEAR' })}>清空购物车</button>
</>
)}
</div>
);
}
核心变化:组件里不再有任何修改逻辑,只有 dispatch({ type: '...' })。所有的”怎么改”都封装在 cartReducer 里,它是一个纯函数,可以单独测试,也可以在多个组件间共享。
1.3 什么时候该用 useReducer
一句话原则:如果 state 是一个对象或数组,并且有多种修改方式,就用 useReducer;如果只是一个简单的布尔值或数字,useState 足矣。
| 场景 | 推荐方案 |
|---|---|
| 开关切换、计数器 | useState |
| 表单(多字段联动) | useReducer |
| 购物车、待办事项列表 | useReducer |
| 复杂的多步骤流程 | useReducer |
🤔 想一想 reducer 函数里为什么不能直接修改原来的 state(比如
state.push(newItem); return state),而必须返回一个新对象?提示:这和 React 的渲染机制有什么关系?
二、useContext —— 跨层级传递数据,告别 props drilling
2.1 Props Drilling:数据的”层层快递”
想象这样一个场景:你的应用有一个”主题色”配置,需要从最顶层的 App 组件一路传递到深层嵌套的按钮组件。如果纯靠 props,你不得不在每一层中间组件里加上 theme 属性——即使这些中间组件本身根本不需要主题色。这就像寄快递必须经过五个中转站,每个中转站都要打开包裹看一眼再转发——纯属浪费。
// Props Drilling 的噩梦
function App() {
const theme = 'dark';
return <Layout theme={theme} />; // Layout 不关心 theme
}
function Layout({ theme }) {
return <Sidebar theme={theme} />; // Sidebar 不关心 theme
}
function Sidebar({ theme }) {
return <NavMenu theme={theme} />; // NavMenu 不关心 theme
}
function NavMenu({ theme }) {
return <button className={theme}>菜单</button>; // 只有这里真正需要
}
2.2 用 Context 建一条”直达隧道”
Context 的思路很简单:在顶层”广播”数据,在任意深度”接收”数据,中间层完全不用参与。 就像在公司里发一条全员通知,每个人自己打开邮箱就能看到,不需要层层转发。
import { createContext, useContext, useState } from 'react';
// 第一步:创建 Context(创建一个"广播频道")
const ThemeContext = createContext('light');
// 第二步:在顶层提供数据(在频道里播报内容)
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<div className={`app ${theme}`}>
<h1>我的应用</h1>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
切换主题
</button>
<Layout />
</div>
</ThemeContext.Provider>
);
}
// 中间层完全不用传 props
function Layout() {
return <Sidebar />;
}
function Sidebar() {
return <NavMenu />;
}
// 第三步:在需要的地方消费数据(调频收听)
function NavMenu() {
const theme = useContext(ThemeContext);
return (
<button style={{
background: theme === 'dark' ? '#333' : '#fff',
color: theme === 'dark' ? '#fff' : '#333',
}}>
当前主题:{theme}
</button>
);
}
2.3 useReducer + useContext = 轻量级状态管理
把前面学的 useReducer 和 useContext 组合起来,你就能实现一个轻量级的全局状态管理——不需要引入 Redux 或 Zustand。
import { createContext, useContext, useReducer } from 'react';
// 创建 Context
const CartContext = createContext(null);
// 复用之前的 cartReducer
function CartProvider({ children }) {
const [cart, dispatch] = useReducer(cartReducer, []);
return (
<CartContext.Provider value={{ cart, dispatch }}>
{children}
</CartContext.Provider>
);
}
// 在任意子组件中使用
function AddToCartButton({ product }) {
const { dispatch } = useContext(CartContext);
return (
<button onClick={() => dispatch({ type: 'ADD_ITEM', product })}>
加入购物车
</button>
);
}
function CartBadge() {
const { cart } = useContext(CartContext);
const count = cart.reduce((sum, item) => sum + item.quantity, 0);
return <span>购物车({count})</span>;
}
适用场景:主题、语言、登录态、简单的全局状态。当应用状态复杂到一定程度时,再考虑 Zustand、Jotai 等专业方案。
三、useMemo —— 缓存计算结果,避免重复运算
3.1 问题:昂贵的计算每次都在重跑
function ProductList({ products, filterText }) {
// 每次渲染都会重新过滤和排序 —— 如果有 10000 条数据呢?
const filtered = products
.filter(p => p.name.includes(filterText))
.sort((a, b) => a.price - b.price);
return (
<ul>
{filtered.map(p => <li key={p.id}>{p.name} - ¥{p.price}</li>)}
</ul>
);
}
组件每次重新渲染,过滤和排序逻辑都会重新执行。如果 products 有上万条,而触发渲染的原因可能只是一个无关的 state 变化,这就是在做无用功。
3.2 用 useMemo 缓存计算结果
useMemo 的作用就像给计算结果加了一层缓存:只有当依赖项真正变化时,才重新计算;否则直接返回上次的结果。
import { useMemo } from 'react';
function ProductList({ products, filterText }) {
// 只有当 products 或 filterText 变化时才重新计算
const filtered = useMemo(() => {
console.log('正在过滤和排序...');
return products
.filter(p => p.name.includes(filterText))
.sort((a, b) => a.price - b.price);
}, [products, filterText]);
return (
<ul>
{filtered.map(p => <li key={p.id}>{p.name} - ¥{p.price}</li>)}
</ul>
);
}
打个比方:useMemo 就像考试时的”草稿纸”。如果题目(依赖项)没变,你直接看草稿纸上的答案就好,不用重新算一遍。
3.3 不要滥用 useMemo
一个常见的误区是给所有计算都加上 useMemo。但 useMemo 本身也有成本——它需要存储上次的值、比较依赖项。对于简单的计算(比如 a + b),useMemo 的开销反而比直接计算更大。
原则:只在”计算量明显很大”或”需要保持引用稳定”时使用 useMemo。
四、useCallback —— 缓存函数引用,配合 memo 阻止不必要的重渲染
4.1 问题:函数引用每次都是新的
在 JavaScript 中,每次函数组件执行,内部定义的函数都是全新创建的。即使函数体一模一样,两次创建的函数也不相等:
function Parent() {
// 每次 Parent 渲染,handleClick 都是一个新函数
const handleClick = () => console.log('clicked');
return <Child onClick={handleClick} />;
}
如果 Child 是一个用 memo 包裹的组件(下一节会讲),它会通过浅比较来判断 props 是否变化。由于 handleClick 每次都是新的引用,memo 会认为”props 变了”,从而触发不必要的重渲染。
4.2 useCallback 锁住函数引用
import { useCallback, useState } from 'react';
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// 只有当 count 变化时,才会生成新的函数
const handleIncrement = useCallback(() => {
setCount(c => c + 1);
}, []); // 使用函数式更新,不依赖外部 count
return (
<div>
<p>计数:{count}</p>
<input value={text} onChange={e => setText(e.target.value)} />
<ExpensiveChild onClick={handleIncrement} />
</div>
);
}
现在,即使 text 变化导致 Parent 重新渲染,handleIncrement 的引用始终不变,ExpensiveChild 就不会被误伤。
4.3 useMemo 和 useCallback 的关系
从本质上讲,useCallback(fn, deps) 完全等价于 useMemo(() => fn, deps)。前者缓存的是函数本身,后者缓存的是函数的返回值。React 提供 useCallback 只是为了让代码更语义化。
| Hook | 缓存对象 | 典型场景 |
|---|---|---|
| useMemo | 计算结果(值) | 大量数据过滤、排序、格式化 |
| useCallback | 函数引用 | 传递给 memo 子组件的回调函数 |
五、memo —— 组件级别的渲染优化
5.1 默认行为:父组件渲染 = 子组件一起渲染
React 的默认规则是:当父组件重新渲染时,所有子组件都会跟着重新渲染,不管子组件的 props 有没有变化。这就像班主任说”全班留下来打扫卫生”,即使你的座位一尘不染。
5.2 memo:给组件发一张”免打扰”通行证
import { memo, useState, useCallback, useMemo } from 'react';
// 用 memo 包裹的组件:只有 props 变化才重新渲染
const ProductCard = memo(function ProductCard({ name, price, onBuy }) {
console.log(`ProductCard 渲染: ${name}`);
return (
<div style={{ border: '1px solid #ccc', padding: 12, margin: 8 }}>
<h3>{name}</h3>
<p>¥{price}</p>
<button onClick={onBuy}>购买</button>
</div>
);
});
function Store() {
const [keyword, setKeyword] = useState('');
const [cartCount, setCartCount] = useState(0);
// useCallback 锁住函数引用,配合 memo 生效
const handleBuy = useCallback(() => {
setCartCount(c => c + 1);
}, []);
// useMemo 保持对象引用稳定
const products = useMemo(() => [
{ id: 1, name: 'React实战', price: 59 },
{ id: 2, name: 'TypeScript手册', price: 49 },
], []);
return (
<div>
<input
placeholder="搜索..."
value={keyword}
onChange={e => setKeyword(e.target.value)}
/>
<p>购物车数量:{cartCount}</p>
{products.map(p => (
<ProductCard key={p.id} name={p.name} price={p.price} onBuy={handleBuy} />
))}
</div>
);
}
在这个例子中,当你在搜索框输入文字时,Store 组件会重新渲染。但由于 ProductCard 被 memo 包裹,且它的 props(name、price、onBuy)都没变化,所以 ProductCard 不会重新渲染。在控制台中你不会看到 “ProductCard 渲染” 的日志。
5.3 三者必须配套使用
这是一个极其重要的结论,请记住:
- 只用 memo,不用 useCallback/useMemo:传递给子组件的函数或对象每次都是新引用,memo 的浅比较发现”props 变了”,优化失效。
- 只用 useCallback/useMemo,不用 memo:子组件没有浅比较逻辑,不管 props 变没变都会重渲染,缓存了个寂寞。
- 三者配套:memo 做”守门员”,useCallback/useMemo 保证传进来的 props 引用稳定。这才是完整的优化链路。
📌 前瞻:React Compiler React Compiler(已于 2025 年 10 月稳定发布)可以在编译阶段自动完成大部分 memo 优化,未来手动写 memo/useMemo/useCallback 的必要性会大幅降低。注意 React Compiler 是一个独立的编译工具,需要单独安装(如
babel-plugin-react-compiler),并非 React 19 包的内置功能。但理解”为什么需要 memo”的原理仍然重要——它帮助你在编译器无法覆盖的场景中做出正确判断。
🤔 想一想 如果给 memo 包裹的子组件传了一个内联的对象字面量(如
style={{ color: 'red' }}),memo 还能生效吗?为什么?
六、自定义 Hook —— 抽取可复用逻辑
6.1 什么是自定义 Hook
自定义 Hook 就是一个以 use 开头的普通函数,内部可以调用其他 Hook。它的本质是把组件中可复用的状态逻辑提取出来,形成一个独立的模块。
打个比方:如果 React 内置的 Hook(useState、useEffect 等)是”原材料”,那么自定义 Hook 就是你用这些原材料拼装出来的”预制件”——别的组件直接拿去用就行,不需要关心内部细节。
6.2 实战:useLocalStorage —— 带本地持久化的 useState
很多场景下,我们希望 state 能在页面刷新后保留。比如用户的主题偏好、表单的草稿数据。useLocalStorage 就是一个增强版的 useState,它在每次 setState 的时候自动同步到 localStorage。
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// 初始化:优先从 localStorage 读取,没有就用 initialValue
// 注意:加了 typeof window 判断,兼容 SSR 环境(Next.js 等)
const [value, setValue] = useState(() => {
if (typeof window === 'undefined') return initialValue;
try {
const stored = window.localStorage.getItem(key);
return stored !== null ? JSON.parse(stored) : initialValue;
} catch {
return initialValue;
}
});
// 每次 value 变化,同步写入 localStorage
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.warn(`无法写入 localStorage key "${key}":`, error);
}
}, [key, value]);
return [value, setValue];
}
使用方式:和 useState 一模一样,多了一个”持久化”超能力。
function Settings() {
const [theme, setTheme] = useLocalStorage('app-theme', 'light');
const [fontSize, setFontSize] = useLocalStorage('font-size', 16);
return (
<div>
<p>当前主题:{theme}</p>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
切换主题
</button>
<p>字体大小:{fontSize}px</p>
<button onClick={() => setFontSize(s => s + 2)}>放大</button>
<button onClick={() => setFontSize(s => Math.max(12, s - 2))}>缩小</button>
</div>
);
}
刷新页面,你会发现设置还在——这就是 useLocalStorage 的威力。
6.3 实战:useFetch —— 声明式的数据请求
几乎每个组件都要请求数据,每次都写 loading、error、data 三件套太烦了。封装一个 useFetch,让数据请求变得声明式。
import { useState, useEffect, useRef } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// 用 ref 追踪组件是否已卸载,避免在卸载后 setState
const isMounted = useRef(true);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
useEffect(() => {
// 用于取消请求的 AbortController
const controller = new AbortController();
async function fetchData() {
setLoading(true);
setError(null);
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const json = await response.json();
if (isMounted.current) {
setData(json);
}
} catch (err) {
if (err.name !== 'AbortError' && isMounted.current) {
setError(err.message);
}
} finally {
if (isMounted.current) {
setLoading(false);
}
}
}
fetchData();
// 清理函数:URL 变化或组件卸载时取消上次请求
return () => {
controller.abort();
};
}, [url]);
return { data, loading, error };
}
使用方式:
function UserList() {
const { data: users, loading, error } = useFetch('https://jsonplaceholder.typicode.com/users');
if (loading) return <p>加载中...</p>;
if (error) return <p>出错了:{error}</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name} - {user.email}</li>
))}
</ul>
);
}
对比一下没有 useFetch 时,你需要在每个组件里写多少重复代码——useState 三个、useEffect 里面 try-catch、AbortController、isMounted 检查…现在只需一行就搞定。
6.4 实战:useDebounce —— 防抖利器
搜索框是最典型的防抖场景:用户每敲一个字母就触发一次搜索请求太浪费了。我们希望用户停止输入 300ms 后,才发起请求。
import { useState, useEffect } from 'react';
function useDebounce(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
// 设定定时器:delay 毫秒后更新值
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// 清理函数:如果 value 在 delay 内又变了,清除上一个定时器
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}
使用方式:
function SearchBox() {
const [keyword, setKeyword] = useState('');
const debouncedKeyword = useDebounce(keyword, 500);
// 只有 debouncedKeyword 变化时才发起请求
const { data, loading } = useFetch(
debouncedKeyword
? `https://api.example.com/search?q=${debouncedKeyword}`
: null
);
return (
<div>
<input
placeholder="搜索..."
value={keyword}
onChange={e => setKeyword(e.target.value)}
/>
{loading && <p>搜索中...</p>}
{data && (
<ul>
{data.results.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
)}
</div>
);
}
注意这里的”积木拼装”效果:useDebounce + useFetch 组合使用,自定义 Hook 之间天然可以互相配合。
🤔 想一想
useDebounce内部用了 useEffect 的清理函数来取消上一个定时器。如果不写清理函数,会发生什么?在什么场景下这个 bug 会特别明显?
七、自定义 Hook 的设计原则
写出一个能用的自定义 Hook 不难,但要写出一个好用、通用、易维护的自定义 Hook,需要遵循一些设计原则。
7.1 命名:必须以 use 开头
这不仅仅是一个约定。React 的 linter 规则(eslint-plugin-react-hooks)会检查以 use 开头的函数,确保你在里面正确地使用了 Hook 的规则(不在条件语句里调用 Hook 等)。如果你的函数叫 getLocalStorage 而不是 useLocalStorage,lint 就无法帮你检查潜在的错误。
好的命名应该一眼就能看出这个 Hook 做什么:
| 命名 | 含义 |
|---|---|
useLocalStorage | 管理 localStorage 中的状态 |
useFetch | 发起数据请求 |
useDebounce | 对值做防抖处理 |
useWindowSize | 监听窗口尺寸变化 |
useClickOutside | 检测点击是否在元素外部 |
7.2 职责单一:一个 Hook 只做一件事
就像函数设计的”单一职责原则”一样,一个自定义 Hook 应该只解决一个问题。如果你发现一个 Hook 的代码超过了 50 行,或者它的名字很难用一个短语概括,那它可能需要被拆分。
反面例子:useFormWithValidationAndSubmit —— 太多事情糅在一起。
正面做法:拆分成 useFormFields(管理字段值)、useValidation(校验逻辑)、useFormSubmit(提交逻辑),然后在组件里组合使用。
7.3 返回值设计:数组还是对象?
- 返回数组:当返回值只有 2-3 个,且用户可能想自定义变量名时。典型代表:
useState返回[value, setValue]。
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [lang, setLang] = useLocalStorage('lang', 'zh');
- 返回对象:当返回值较多、名字有明确含义时。典型代表:
useFetch返回{ data, loading, error }。
const { data, loading, error } = useFetch('/api/users');
一个实用的判断标准:如果同一个组件里可能多次调用同一个 Hook,优先用数组(解构重命名更方便);否则用对象(语义更清晰)。
7.4 让 Hook 保持”纯净”
自定义 Hook 不应该关心 UI 的细节。它只负责逻辑,不返回 JSX。如果你的 Hook 里出现了 <div> 或 <button>,那它应该是一个组件而不是 Hook。
八、实战:构建一组实用的自定义 Hook 工具库
让我们再补充几个实用的自定义 Hook,形成一个小型工具库。
8.1 useWindowSize —— 响应式窗口尺寸
import { useState, useEffect } from 'react';
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
function handleResize() {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
使用方式:
function ResponsiveLayout() {
const { width } = useWindowSize();
return (
<div>
{width < 768 ? <MobileNav /> : <DesktopNav />}
<p>当前宽度:{width}px</p>
</div>
);
}
8.2 useClickOutside —— 点击外部检测
模态框、下拉菜单的经典需求:点击元素外部时关闭。
import { useEffect, useRef } from 'react';
function useClickOutside(handler) {
const ref = useRef(null);
const handlerRef = useRef(handler);
// 始终保存最新的 handler,避免调用方需要用 useCallback 包裹
useEffect(() => {
handlerRef.current = handler;
}, [handler]);
useEffect(() => {
function handleClick(event) {
// 如果点击的目标不在 ref 元素内部,触发回调
if (ref.current && !ref.current.contains(event.target)) {
handlerRef.current();
}
}
document.addEventListener('mousedown', handleClick);
return () => document.removeEventListener('mousedown', handleClick);
}, []); // 不依赖 handler,通过 ref 访问最新值
return ref;
}
使用方式:
function Dropdown() {
const [open, setOpen] = useState(false);
const dropdownRef = useClickOutside(() => setOpen(false));
return (
<div ref={dropdownRef}>
<button onClick={() => setOpen(o => !o)}>菜单</button>
{open && (
<ul style={{ border: '1px solid #ccc', padding: 8 }}>
<li>选项一</li>
<li>选项二</li>
<li>选项三</li>
</ul>
)}
</div>
);
}
8.3 useToggle —— 布尔值切换
最简单但最高频的需求:
import { useState, useCallback } from 'react';
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return [value, { toggle, setTrue, setFalse }];
}
使用方式:
function Modal() {
const [visible, { toggle, setFalse }] = useToggle(false);
return (
<div>
<button onClick={toggle}>打开弹窗</button>
{visible && (
<div className="modal">
<p>弹窗内容</p>
<button onClick={setFalse}>关闭</button>
</div>
)}
</div>
);
}
8.4 工具库全景
到这里,我们已经构建了一套实用的自定义 Hook 工具库:
| Hook | 功能 | 返回值 |
|---|---|---|
useLocalStorage | 持久化的 useState | [value, setValue] |
useFetch | 声明式数据请求 | { data, loading, error } |
useDebounce | 值防抖 | debouncedValue |
useWindowSize | 响应式窗口尺寸 | { width, height } |
useClickOutside | 点击外部检测 | ref |
useToggle | 布尔值切换 | [value, { toggle, setTrue, setFalse }] |
这些 Hook 就像乐高积木:每一块都小巧独立,但组合起来可以搭建出任意复杂的功能。当你发现自己在多个组件里写了类似的逻辑时,就是时候把它抽成自定义 Hook 了。
📝 掌握度自测
-
请用 useReducer 实现一个简单的待办事项(Todo)管理,支持添加、删除、切换完成状态三个操作。你的 reducer 函数应该是怎样的?
-
如果你有一个深层嵌套的组件树,顶层组件管理”当前登录用户”信息,最底层的 Avatar 组件需要用到用户头像 URL。请描述你会如何用 useContext 解决这个问题,并写出关键代码。
-
下面这段代码的 memo 优化为什么失效了?请指出问题并修复。
const Child = memo(({ items, onSelect }) => { /* ... */ }); function Parent() { const [query, setQuery] = useState(''); const items = data.filter(d => d.name.includes(query)); const onSelect = (id) => console.log(id); return <Child items={items} onSelect={onSelect} />; } -
请实现一个
usePrevious自定义 Hook,它能返回某个值在上一次渲染时的状态。提示:你需要用到 useRef 和 useEffect。 -
在设计自定义 Hook 的返回值时,什么场景用数组,什么场景用对象?请各举一个例子说明理由。
💡 自我评估:
- 答对 1-2 题:你已经理解了进阶 Hook 的基本概念,建议把本章的每个代码示例都手敲一遍运行起来。
- 答对 3-4 题:你对 React 性能优化和自定义 Hook 有了不错的掌握,可以开始在实际项目中应用了。
- 答对 5 题:你已经具备了独立设计和封装 Hook 的能力,可以继续深入学习组件设计模式了。
购买课程解锁全部内容
从组件到架构:12 章系统掌握现代 React
¥29.90