1. React Hooks 核心概念解析
React Hooks 是 React 16.8 引入的革命性特性,它彻底改变了我们在函数组件中管理状态和副作用的方式。作为一名长期使用 React 的开发人员,我深刻体会到 Hooks 带来的开发体验提升。让我们从最基础的数据驱动开始,逐步深入理解 Hooks 的各个核心功能。
1.1 数据驱动:useState 深度剖析
useState 是 React 中最基础也是最重要的 Hook,它让我们在函数组件中能够拥有状态管理能力。理解它的工作原理对于构建可靠的 React 应用至关重要。
1.1.1 基本数据类型使用
让我们从一个最简单的计数器示例开始:
javascript复制import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>当前计数: {count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
);
}
这个例子展示了 useState 的基本用法:
- 调用 useState 并传入初始值(这里是 0)
- 返回一个数组,包含当前状态值和更新函数
- 通过解构赋值获取 count 和 setCount
重要提示:useState 的初始值只在组件首次渲染时使用,后续渲染会忽略这个初始值。
1.1.2 复杂数据类型处理
当处理数组和对象等复杂数据类型时,需要特别注意 React 的不可变数据原则。让我们看一个购物车示例:
javascript复制function ShoppingCart() {
const [items, setItems] = useState([
{ id: 1, name: '苹果', price: 10, quantity: 1 },
{ id: 2, name: '香蕉', price: 5, quantity: 2 }
]);
const addItem = () => {
// 错误做法:直接修改原数组
// items.push({ id: 3, name: '橙子', price: 8, quantity: 1 });
// 正确做法:创建新数组
setItems([...items, { id: 3, name: '橙子', price: 8, quantity: 1 }]);
};
const updateQuantity = (id, newQuantity) => {
setItems(items.map(item =>
item.id === id ? { ...item, quantity: newQuantity } : item
));
};
return (
<div>
<ul>
{items.map(item => (
<li key={item.id}>
{item.name} - 数量:
<input
type="number"
value={item.quantity}
onChange={(e) => updateQuantity(item.id, parseInt(e.target.value))}
/>
</li>
))}
</ul>
<button onClick={addItem}>添加商品</button>
</div>
);
}
处理复杂状态时需要注意:
- 永远不要直接修改原状态
- 使用展开运算符(...)创建新对象/数组
- 对于嵌套对象,需要层层展开
1.1.3 函数式更新
当新状态依赖于旧状态时,应该使用函数式更新:
javascript复制function Counter() {
const [count, setCount] = useState(0);
// 不推荐:连续调用可能不会按预期工作
const incrementTwice = () => {
setCount(count + 1);
setCount(count + 1); // 这两行实际上只会增加1
};
// 推荐:使用函数式更新
const incrementTwiceCorrect = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1); // 这会正确增加2
};
return (
<div>
<p>计数: {count}</p>
<button onClick={incrementTwice}>错误增加两次</button>
<button onClick={incrementTwiceCorrect}>正确增加两次</button>
</div>
);
}
函数式更新的优势:
- 确保基于最新状态进行更新
- 避免批量更新导致的问题
- 在闭包中也能获取正确状态
1.2 useReducer:集中式状态管理
当组件状态逻辑变得复杂时,useReducer 是一个更好的选择。它借鉴了 Redux 的思想,将状态更新逻辑集中管理。
1.2.1 基础计数器实现
javascript复制import { useReducer } from 'react';
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, { 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>
</div>
);
}
useReducer 的三个关键部分:
- reducer 函数:定义状态更新逻辑
- 初始状态
- dispatch 函数:触发状态更新
1.2.2 购物车完整案例
让我们实现一个更复杂的购物车示例:
javascript复制function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
return {
...state,
items: [...state.items, action.payload],
total: state.total + action.payload.price * action.payload.quantity
};
case 'REMOVE_ITEM':
const removedItem = state.items.find(item => item.id === action.payload);
return {
...state,
items: state.items.filter(item => item.id !== action.payload),
total: state.total - removedItem.price * removedItem.quantity
};
case 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
),
total: state.items.reduce((sum, item) => {
const quantity = item.id === action.payload.id
? action.payload.quantity
: item.quantity;
return sum + item.price * quantity;
}, 0)
};
default:
return state;
}
}
function ShoppingCart() {
const [state, dispatch] = useReducer(cartReducer, {
items: [],
total: 0
});
const [newItem, setNewItem] = useState({ name: '', price: 0, quantity: 1 });
const addItem = () => {
dispatch({
type: 'ADD_ITEM',
payload: { ...newItem, id: Date.now() }
});
setNewItem({ name: '', price: 0, quantity: 1 });
};
return (
<div>
<h2>购物车</h2>
<div>
<input
value={newItem.name}
onChange={(e) => setNewItem({...newItem, name: e.target.value})}
placeholder="商品名称"
/>
<input
type="number"
value={newItem.price}
onChange={(e) => setNewItem({...newItem, price: parseFloat(e.target.value)})}
placeholder="价格"
/>
<input
type="number"
value={newItem.quantity}
onChange={(e) => setNewItem({...newItem, quantity: parseInt(e.target.value)})}
placeholder="数量"
/>
<button onClick={addItem}>添加商品</button>
</div>
<ul>
{state.items.map(item => (
<li key={item.id}>
{item.name} - ¥{item.price} ×
<input
type="number"
value={item.quantity}
onChange={(e) => dispatch({
type: 'UPDATE_QUANTITY',
payload: { id: item.id, quantity: parseInt(e.target.value) }
})}
/>
<button onClick={() => dispatch({ type: 'REMOVE_ITEM', payload: item.id })}>
删除
</button>
</li>
))}
</ul>
<p>总价: ¥{state.total.toFixed(2)}</p>
</div>
);
}
useReducer 的优势:
- 集中管理复杂状态逻辑
- 状态更新更可预测
- 便于测试和复用
- 适合处理包含多个子值的复杂状态
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 副作用处理:useEffect 与 useLayoutEffect
副作用是 React 组件中与外部世界交互的操作,如数据获取、订阅、手动修改 DOM 等。理解如何正确使用 useEffect 是掌握 React Hooks 的关键。
2.1 useEffect 深度解析
useEffect 是处理副作用的主要 Hook,它取代了类组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 生命周期方法。
2.1.1 基本用法
javascript复制import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true; // 用于避免组件卸载后设置状态
const fetchUser = async () => {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
const data = await response.json();
if (isMounted) {
setUser(data);
setLoading(false);
}
} catch (error) {
if (isMounted) {
setLoading(false);
console.error('获取用户数据失败:', error);
}
}
};
fetchUser();
return () => {
isMounted = false; // 清理函数
};
}, [userId]); // 依赖数组
if (loading) return <div>加载中...</div>;
if (!user) return <div>未找到用户</div>;
return (
<div>
<h2>{user.name}</h2>
<p>邮箱: {user.email}</p>
<p>电话: {user.phone}</p>
</div>
);
}
useEffect 的核心概念:
- 副作用函数:包含要执行的副作用代码
- 依赖数组:决定何时重新执行副作用
- 清理函数:返回的函数会在组件卸载或依赖变化前执行
2.1.2 依赖数组详解
依赖数组决定了 useEffect 的执行时机:
-
无依赖数组:每次渲染后都执行
javascript复制useEffect(() => { // 每次渲染后都会执行 }); -
空依赖数组:仅在组件挂载时执行
javascript复制useEffect(() => { // 仅在组件挂载时执行 }, []); -
有依赖项:依赖项变化时执行
javascript复制useEffect(() => { // 当userId或pageSize变化时执行 }, [userId, pageSize]);
重要提示:确保依赖数组中包含所有在副作用函数中使用的外部值(props、state等),否则可能导致闭包问题。
2.1.3 清理机制
useEffect 的清理函数在以下时机执行:
- 组件卸载时
- 依赖项变化导致副作用重新执行前
javascript复制useEffect(() => {
const timer = setInterval(() => {
console.log('定时器执行');
}, 1000);
return () => {
clearInterval(timer); // 清理定时器
console.log('清理定时器');
};
}, []);
常见需要清理的资源:
- 定时器
- 事件监听器
- WebSocket连接
- 订阅
2.2 useLayoutEffect 特殊场景应用
useLayoutEffect 与 useEffect 的API相同,但执行时机不同。它在DOM更新后、浏览器绘制前同步执行,适合需要读取DOM布局或同步更新DOM的场景。
2.2.1 与 useEffect 的区别
javascript复制import { useState, useEffect, useLayoutEffect } from 'react';
function Example() {
const [width, setWidth] = useState(0);
const divRef = useRef();
useEffect(() => {
console.log('useEffect - width:', width);
// 这里可能会有闪烁,因为浏览器可能已经绘制了
}, [width]);
useLayoutEffect(() => {
console.log('useLayoutEffect - width:', width);
// 在这里测量或更新DOM不会导致闪烁
const newWidth = divRef.current.offsetWidth;
if (newWidth !== width) {
setWidth(newWidth);
}
}, []);
return (
<div ref={divRef} style={{ width: '100%', backgroundColor: 'lightblue' }}>
<p>宽度: {width}px</p>
</div>
);
}
关键区别:
- useEffect:异步执行,不阻塞浏览器绘制
- useLayoutEffect:同步执行,阻塞浏览器绘制
2.2.2 适用场景
- 测量DOM元素
- 同步更新DOM以避免闪烁
- 需要确保DOM状态一致的情况
滚动位置保持示例:
javascript复制function ScrollPosition() {
const [scrollTop, setScrollTop] = useState(0);
const containerRef = useRef();
useLayoutEffect(() => {
const container = containerRef.current;
const handleScroll = () => {
setScrollTop(container.scrollTop);
};
container.addEventListener('scroll', handleScroll);
// 恢复滚动位置
container.scrollTop = scrollTop;
return () => {
container.removeEventListener('scroll', handleScroll);
};
}, []);
return (
<div
ref={containerRef}
style={{ height: '200px', overflow: 'auto' }}
>
{Array.from({ length: 50 }).map((_, i) => (
<div key={i}>项目 {i + 1}</div>
))}
</div>
);
}
性能提示:大多数情况下应该优先使用 useEffect,只有在需要同步测量或更新DOM时才使用 useLayoutEffect,因为它会阻塞浏览器绘制。
3. 状态传递与共享
在React应用中,组件之间共享状态是常见需求。React提供了多种方式来实现状态传递和共享,从简单的props传递到复杂的上下文API。
3.1 useRef:DOM操作与数据存储
useRef 主要有两个用途:访问DOM元素和在组件渲染间保存可变值。
3.1.1 DOM元素操作
javascript复制import { useRef } from 'react';
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
inputEl.current.focus();
console.log(inputEl.current.value);
};
return (
<div>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>聚焦输入框</button>
</div>
);
}
useRef 操作DOM的特点:
- 创建一个 ref 对象并赋值给DOM元素的ref属性
- current 属性指向实际的DOM元素
- 修改 ref.current 不会触发组件重新渲染
3.1.2 数据存储与跨渲染持久化
javascript复制import { useState, useRef, useEffect } from 'react';
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef();
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(intervalRef.current);
}, []);
const stopTimer = () => {
clearInterval(intervalRef.current);
};
return (
<div>
<p>计时: {count}秒</p>
<button onClick={stopTimer}>停止</button>
</div>
);
}
useRef 存储数据的特点:
- 返回的对象在组件的整个生命周期内保持不变
- 修改 current 属性不会触发重新渲染
- 适合存储不需要触发渲染的可变值
3.1.3 注意事项与最佳实践
- 避免在渲染期间修改 ref.current
- ref 不是响应式的 - 修改不会触发重新渲染
- 不要过度使用 ref 来存储状态 - 应该优先考虑 useState 或 useReducer
- 使用 forwardRef 暴露DOM ref给父组件
javascript复制const FancyInput = React.forwardRef((props, ref) => {
return <input ref={ref} className="fancy" {...props} />;
});
function App() {
const inputRef = useRef();
const focusInput = () => {
inputRef.current.focus();
};
return (
<div>
<FancyInput ref={inputRef} />
<button onClick={focusInput}>聚焦输入框</button>
</div>
);
}
3.2 useContext:跨组件状态共享
useContext 允许我们在组件树中共享数据,而无需显式地通过每一层组件传递props。
3.2.1 基础使用模式
javascript复制import { createContext, useContext, useState } from 'react';
// 1. 创建上下文
const ThemeContext = createContext({
theme: 'light',
toggleTheme: () => {}
});
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
function ThemedButton() {
// 3. 使用上下文
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
onClick={toggleTheme}
style={{
backgroundColor: theme === 'dark' ? '#333' : '#FFF',
color: theme === 'dark' ? '#FFF' : '#333'
}}
>
切换主题 ({theme})
</button>
);
}
function App() {
return (
<ThemeProvider>
<div>
<ThemedButton />
</div>
</ThemeProvider>
);
}
useContext 使用步骤:
- 使用 createContext 创建上下文
- 使用 Provider 提供值
- 在子组件中使用 useContext 获取值
3.2.2 性能优化技巧
当上下文值变化时,所有使用该上下文的组件都会重新渲染。为了避免不必要的渲染:
- 拆分上下文 - 将不常变化的值和频繁变化的值分开
- 使用 memo 记忆子组件
- 使用 useMemo 记忆上下文值
javascript复制function UserProvider({ children }) {
const [user, setUser] = useState(null);
const [preferences, setPreferences] = useState({});
// 将不常变化的值和频繁变化的值分开
const userContextValue = useMemo(() => ({
user,
login: (userData) => setUser(userData),
logout: () => setUser(null)
}), [user]);
const preferencesContextValue = useMemo(() => ({
preferences,
updatePreferences: (newPrefs) => setPreferences(newPrefs)
}), [preferences]);
return (
<UserContext.Provider value={userContextValue}>
<PreferencesContext.Provider value={preferencesContextValue}>
{children}
</PreferencesContext.Provider>
</UserContext.Provider>
);
}
const UserProfile = React.memo(() => {
const { user } = useContext(UserContext);
// 只有当user变化时才会重新渲染
return <div>{user?.name}</div>;
});
3.2.3 多层嵌套上下文处理
当有多个上下文嵌套时,可以使用组合模式:
javascript复制function AppProviders({ children }) {
const [auth, setAuth] = useState(null);
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);
const authValue = useMemo(() => ({ auth, setAuth }), [auth]);
const themeValue = useMemo(() => ({ theme, setTheme }), [theme]);
const notificationValue = useMemo(() => ({
notifications,
addNotification: (msg) => setNotifications([...notifications, msg])
}), [notifications]);
return (
<AuthContext.Provider value={authValue}>
<ThemeContext.Provider value={themeValue}>
<NotificationContext.Provider value={notificationValue}>
{children}
</NotificationContext.Provider>
</ThemeContext.Provider>
</AuthContext.Provider>
);
}
function App() {
return (
<AppProviders>
<MainLayout />
</AppProviders>
);
}
4. 状态派生与性能优化
在React应用中,派生状态和性能优化是提升应用响应速度的关键。React提供了useMemo和useCallback等Hook来帮助我们高效地处理这些问题。
4.1 useMemo:记忆化计算结果
useMemo用于记忆化计算结果,避免在每次渲染时都进行昂贵的计算。
4.1.1 基础用法
javascript复制import { useMemo, useState } from 'react';
function ExpensiveCalculation({ list }) {
const [filter, setFilter] = useState('');
const filteredList = useMemo(() => {
console.log('重新计算过滤列表');
return list.filter(item =>
item.toLowerCase().includes(filter.toLowerCase())
);
}, [list, filter]);
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="过滤条件"
/>
<ul>
{filteredList.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}
useMemo的特点:
- 接受一个计算函数和依赖数组
- 只有当依赖项变化时才重新计算
- 返回记忆化的值
4.1.2 React.memo与useMemo结合使用
React.memo是一个高阶组件,用于记忆化组件渲染结果。
javascript复制const ExpensiveComponent = React.memo(({ data }) => {
// 只有当props变化时才会重新渲染
console.log('ExpensiveComponent渲染');
return <div>{JSON.stringify(data)}</div>;
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [items, setItems] = useState([1, 2, 3]);
const processedData = useMemo(() => {
return items.map(item => ({
value: item,
squared: item * item,
cubed: item * item * item
}));
}, [items]);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>计数: {count}</button>
<ExpensiveComponent data={processedData} />
</div>
);
}
这种组合的优势:
- 避免不必要的计算(useMemo)
- 避免不必要的子组件重新渲染(React.memo)
- 只在真正需要时更新
4.1.3 使用场景与注意事项
适合使用useMemo的场景:
- 昂贵的计算
- 引用稳定性(确保对象引用不变)
- 避免不必要的子组件渲染
注意事项:
- 不要过度使用 - 简单的计算可能不需要
- 确保依赖数组完整
- 记忆化本身也有开销
4.2 useCallback:记忆化函数
useCallback用于记忆化函数,确保函数引用在依赖不变时保持稳定。
4.2.1 基础用法
javascript复制import { useState, useCallback } from 'react';
function ParentComponent() {
const [count, setCount] = useState(0);
const [value, setValue] = useState('');
// 普通函数 - 每次渲染都会创建新函数
const handleChange = (e) => {
setValue(e.target.value);
};
// useCallback记忆的函数 - 依赖不变时返回相同引用
const handleSubmit = useCallback(() => {
console.log('提交:', value);
// 发送到服务器...
}, [value]);
return (
<div>
<input value={value} onChange={handleChange} />
<button onClick={() => setCount(c => c + 1)}>计数: {count}</button>
<ChildComponent onSubmit={handleSubmit} />
</div>
);
}
const ChildComponent = React.memo(({ onSubmit }) => {
console.log('ChildComponent渲染');
return <button onClick={onSubmit}>提交</button>;
});
useCallback的特点:
- 返回一个记忆化的回调函数
- 只有当依赖项变化时才创建新函数
- 常用于优化子组件渲染
4.2.2 与useMemo的关系
useCallback(fn, deps) 等价于 useMemo(() => fn, deps)
javascript复制// 这两种写法是等价的
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
const memoizedCallback = useMemo(() => () => {
doSomething(a, b);
}, [a, b]);
何时使用哪个:
- useCallback - 记忆化函数
- useMemo - 记忆化任何类型的值
4.2.3 常见问题与解决方案
问题1:依赖循环
javascript复制const [data, setData] = useState([]);
// 这会导致每次data变化都创建新函数
const addItem = useCallback((item) => {
setData([...data, item]);
}, [data]);
// 解决方案:使用函数式更新
const addItem = useCallback((item) => {
setData(prev => [...prev, item]);
}, []);
问题2:不必要的记忆化
javascript复制// 不需要useCallback - 简单函数没有性能影响
const handleClick = useCallback(() => {
console.log('点击');
}, []);
// 更简单的写法
const handleClick = () => {
console.log('点击');
};
问题3:过度嵌套的回调
javascript复制// 难以维护的深度嵌套回调
const handleComplexAction = useCallback(() => {
api.fetchData().then(response => {
setData(response.data);
analytics.track('data-fetched').then(() => {
showNotification('数据加载完成');
});
});
}, []);
// 解决方案:拆分为多个函数
const fetchData = useCallback(async () => {
const response = await api.fetchData();
setData(response.data);
return response;
}, []);
const trackAnalytics = useCallback(async () => {
await analytics.track('data-fetched');
}, []);
const handleComplexAction = useCallback(async () => {
await fetchData();
await trackAnalytics();
showNotification('数据加载完成');
}, [fetchData, trackAnalytics]);
5. 实用工具Hooks与自定义Hook
除了React内置的Hooks,我们还可以创建自定义Hook来封装可复用的逻辑。这是Hooks最强大的特性之一。
5.1 常用自定义Hook示例
5.1.1 useFetch:数据获取Hook
javascript复制import { useState, useEffect } from 'react';
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true;
const fetchData = async () => {
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error(response.statusText);
const result = await response.json();
if (isMounted) {
setData(result);
setError(null);
}
} catch (err) {
if (isMounted) {
setError(err.message);
setData(null);
}
} finally {
if (isMounted) setLoading(false);
}
};
fetchData();
return () => {
isMounted = false;
};
}, [url, options]);
return { data, error, loading };
}
// 使用示例
function UserProfile({ userId }) {
const { data: user, error, loading } = useFetch(`/api/users/${userId}`);
if (loading) return <div>加载中...</div>;
if (error) return <div>错误: {error}</div>;
return (
<div>
<h2>{user.name}</h2>
<p>邮箱: {user.email}</p>
</div>
);
}
5.1.2 useLocalStorage:持久化状态Hook
javascript复制import { useState, useEffect } 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(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(error);
}
};
useEffect(() => {
const handleStorageChange = (e) => {
if (e.key === key) {
try {
setStoredValue(e.newValue ? JSON.parse(e.newValue) : initialValue);
} catch (error) {
console.error(error);
}
}
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, [key, initialValue]);
return [storedValue, setValue];
}
// 使用示例
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<button onClick={toggleTheme}>
当前主题: {theme}
</button>
);
}
5.1.3 useDebounce:防抖Hook
javascript复制import { useState, useEffect } from 'react';
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);
const [results, setResults] = useState([]);
useEffect(() => {
if (debouncedQuery) {
fetchResults(debouncedQuery).then(setResults);
} else {
setResults([]);
}
}, [debouncedQuery]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索..."
/>
<ul>
{results.map(result => (
<li key={result.id}>{result.name}</li>
))}
</ul>
</div>
);
}
5.2 自定义Hook最佳实践
5.2.1 命名约定
- 总是以"use"开头 - 这样React才能识别它是Hook
- 使用描述性名称 - 清楚地表达Hook的用途
- 好的例子: useWindowSize, useFormValidation
- 不好的例子: useStuff, useMyHook
5.2.2 组合现有Hook
自定义Hook可以组合多个内置Hook:
javascript复制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);
};
}, []);
return isOnline;
}
function useDocumentTitle(title) {
useEffect(() => {
document.title = title;
}, [title]);
}
// 组合使用
function App() {
const isOnline = useOnlineStatus();
useDocumentTitle(isOnline ? '在线' : '离线');
return (
<div>
<h1>{isOnline ? '✅ 在线' : '❌ 离线'}</h1>
</div>
);
}
5.2.3 测试自定义Hook
测试自定义Hook的推荐方式:
javascript复制import { renderHook, act } from '@testing-library/react-hooks';
import { useCounter } from './useCounter';
test('should increment counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test('should accept initial value', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(9);
});
5.3 社区流行Hook库推荐
- react-use - 包含大量实用Hook
- ahooks - 阿里巴巴开源的Hook库
- usehooks-ts - TypeScript友好的Hook集合
- react-hook-form - 表单处理Hook库
- swr - 数据获取Hook库
使用示例(react-use):
javascript复制import { useToggle, useHover } from 'react-use';
function Example() {
const [on, toggle] = useToggle(false);
const [hoverable, hovered] = useHover(<div>悬停我</div>);
return (
<div>
<button onClick={toggle}>{on ? '开' : '关'}</button>
{hoverable}
<div>{hovered ? '悬停中' : '未悬停'}</div>
</div>
);
}
6. React Hooks 高级模式与最佳实践
掌握了Hooks的基础用法后,让我们深入探讨一些高级模式和最佳实践,这些技巧将帮助你构建更健壮、更易维护的React应用。
6.1 依赖数组的精确控制
依赖数组是useEffect、useMemo和useCallback等Hook的关键部分,正确处理依赖关系对于避免bug和性能问题至关重要。
6.1.1 依赖项处理原则
- 诚实列出所有依赖 - 包括props、state和context值
- 使用eslint-plugin-react-hooks插件自动检测
- 当依赖变化过于频繁时考虑重构
javascript复制function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
// 错误:缺少fetchProduct的依赖
const fetchProduct = useCallback(async () => {
const response = await fetch(`/api/products/${productId}`);
const data = await response.json();
setProduct(data);
}, []); // ← 这里缺少productId依赖
useEffect(() => {
fetchProduct();
}, [fetchProduct]);
// 正确:
