useState
和 useEffect
是基础。但在实际开发中,会遇到很多它们无法优雅解决的场景。这篇笔记记录剩下的几个核心 Hooks,以及它们在真实代码中的使用场景、坑点和技巧。
useContext
- 终结 Prop Drilling
-
核心目的: 干掉"道具钻探"(Prop Drilling)。当数据需要跨越多个层级时,用它来建立一个"直通车",避免中间组件无意义的透传。
-
关键坑点与解决方案:性能问题
useContext
的大坑在于,只要Provider
的value
引用地址一变,所有消费这个 Context 的组件都会重渲染。问题示例: 下面的
Toolbar
组件根本没用到counter
,但因为App
组件的counter
状态变化导致重渲,value
属性被重新创建了一个新对象{}
,导致Toolbar
也跟着重渲。// 问题代码:不必要的重渲染 import { createContext, useContext, useState } from 'react' const AppContext = createContext(null) function Toolbar() { // 这个组件理论上只关心 theme,但也被迫重渲了 console.log('Toolbar re-rendered unnecessarily!') const { theme } = useContext(AppContext) return <div>Current theme: {theme}</div> } function App() { const [theme, setTheme] = useState('dark') const [counter, setCounter] = useState(0) // 一个无关的状态 // 每次 App 重渲 (例如点击 button), value 都会是一个全新的对象 const value = { theme, setTheme } return ( <AppContext.Provider value={value}> <button onClick={() => setCounter(c => c + 1)}>Re-render App ({counter})</button> <Toolbar /> </AppContext.Provider> ) }
解决方案:拆分 Context,或对 value 进行
useMemo
缓存 这是解决上述性能问题的最佳实践。把"不变的"和"常变的"数据分开。// 最佳实践:拆分 Context const ThemeContext = createContext(null); // 用于传递 theme const ThemeDispatchContext = createContext(null); // 用于传递 setTheme function App() { const [theme, setTheme] = useState('dark'); // ... return ( <ThemeContext.Provider value={theme}> <ThemeDispatchContext.Provider value={setTheme}> {/* ... */} </ThemeDispatchContext.Provider> </ThemeContext.Provider> ); }
这样,只消费
ThemeDispatchContext
的组件就不会因为theme
的变化而重渲。
useReducer
- 替代繁杂的 useState
- 使用时机: 当一个状态有多种更新逻辑,或者下一个状态依赖于复杂的前置状态时,用
useReducer
会让代码更清晰。
核心语法:
const [state, dispatch] = useReducer(reducer, initialState, init?)
实际使用示例:购物车状态管理
import { useReducer } from 'react';
// 定义状态类型
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
interface CartState {
items: CartItem[];
total: number;
}
// 定义操作类型
type CartAction =
| { type: 'ADD_ITEM'; payload: Omit<CartItem, 'quantity'> }
| { type: 'REMOVE_ITEM'; payload: number }
| { type: 'UPDATE_QUANTITY'; payload: { id: number; quantity: number } }
| { type: 'CLEAR_CART' };
// 初始状态
const initialState: CartState = {
items: [],
total: 0
};
// Reducer 函数
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD_ITEM': {
const existingItem = state.items.find(item => item.id === action.payload.id);
if (existingItem) {
// 如果商品已存在,增加数量
const updatedItems = state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
);
return {
items: updatedItems,
total: updatedItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
};
} else {
// 如果商品不存在,添加新商品
const newItem = { ...action.payload, quantity: 1 };
const updatedItems = [...state.items, newItem];
return {
items: updatedItems,
total: updatedItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
};
}
}
case 'REMOVE_ITEM': {
const updatedItems = state.items.filter(item => item.id !== action.payload);
return {
items: updatedItems,
total: updatedItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
};
}
case 'UPDATE_QUANTITY': {
const updatedItems = state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: Math.max(0, action.payload.quantity) }
: item
).filter(item => item.quantity > 0); // 移除数量为 0 的商品
return {
items: updatedItems,
total: updatedItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
};
}
case 'CLEAR_CART':
return initialState;
default:
return state;
}
}
// 购物车组件
function ShoppingCart() {
const [state, dispatch] = useReducer(cartReducer, initialState);
const addItem = (item: Omit<CartItem, 'quantity'>) => {
dispatch({ type: 'ADD_ITEM', payload: item });
};
const removeItem = (id: number) => {
dispatch({ type: 'REMOVE_ITEM', payload: id });
};
const updateQuantity = (id: number, quantity: number) => {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } });
};
const clearCart = () => {
dispatch({ type: 'CLEAR_CART' });
};
return (
<div className="p-4">
<h2 className="text-xl font-bold mb-4">购物车</h2>
{state.items.length === 0 ? (
<p className="text-gray-500">购物车是空的</p>
) : (
<>
<div className="space-y-2 mb-4">
{state.items.map(item => (
<div key={item.id} className="flex justify-between items-center p-2 border rounded">
<div>
<h3 className="font-medium">{item.name}</h3>
<p className="text-sm text-gray-600">¥{item.price}</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => updateQuantity(item.id, item.quantity - 1)}
className="px-2 py-1 bg-gray-200 rounded"
>
-
</button>
<span className="w-8 text-center">{item.quantity}</span>
<button
onClick={() => updateQuantity(item.id, item.quantity + 1)}
className="px-2 py-1 bg-gray-200 rounded"
>
+
</button>
<button
onClick={() => removeItem(item.id)}
className="ml-2 text-red-500"
>
删除
</button>
</div>
</div>
))}
</div>
<div className="flex justify-between items-center">
<p className="text-lg font-bold">总计: ¥{state.total}</p>
<button
onClick={clearCart}
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
>
清空购物车
</button>
</div>
</>
)}
{/* 示例商品 */}
<div className="mt-6">
<h3 className="font-bold mb-2">添加商品:</h3>
<div className="space-y-2">
<button
onClick={() => addItem({ id: 1, name: '苹果', price: 5 })}
className="block w-full text-left p-2 border rounded hover:bg-gray-50"
>
苹果 - ¥5
</button>
<button
onClick={() => addItem({ id: 2, name: '香蕉', price: 3 })}
className="block w-full text-left p-2 border rounded hover:bg-gray-50"
>
香蕉 - ¥3
</button>
</div>
</div>
</div>
);
}
useMemo
& useCallback
- 性能优化工具
-
使用前提: 不要过早优化,不要滥用。只有在通过 Profiler 发现明确的性能瓶颈时才应该使用。
-
核心坑点:在依赖项数组中使用了不稳定的引用(函数/对象) 这是导致
useMemo
/useCallback
优化失效,甚至起反作用的根源。问题示例: 下面的
options
对象在Parent
组件每次渲染时都会被重新创建,导致useMemo
的依赖项[options]
每次都变化,缓存永远不会命中。function Parent() { const [text, setText] = useState(''); // 错误:options 在每次渲染时都是一个新对象 const options = { delimiter: '-' }; return <ChildComponent options={options} text={text} />; } function ChildComponent({ options, text }) { // 这里的 useMemo 完全无效,因为 options 引用总是在变 const processedText = useMemo(() => { console.log('Expensive calculation running...'); return text.split('').join(options.delimiter); }, [text, options]); return <div>{processedText}</div>; }
解决方案: 将不稳定的依赖项本身也用
useMemo
或useCallback
缓存起来。function Parent() { const [text, setText] = useState(''); // 正确:用 useMemo 缓存 options 对象,它的引用只有在 {} 内部值变化时才变 // 在这个例子中,它永远不会变。 const options = useMemo(() => ({ delimiter: '-' }), []); return <ChildComponent options={options} text={text} />; } // ... ChildComponent 保持不变,现在它的 useMemo 可以正常工作了
useCallback
的主要场景示例:useCallback
是useMemo
针对函数的语法糖。它的核心用途是配合React.memo
。import { useState, useCallback, memo } from 'react'; // 1.用 React.memo 包裹子组件,让它只有在 props 变化时才重渲 const MyButton = memo(({ onClick }) => { console.log('MyButton re-rendered'); return <button onClick={onClick}>Click me</button>; }); function App() { const [count, setCount] = useState(0); // 2. 如果不用 useCallback,每次 App 重渲,这个函数都是新的 // const handleClick = () => { console.log('clicked!'); }; // 3. 用 useCallback 包裹后,handleClick 的引用就稳定了 const handleClick = useCallback(() => { console.log('clicked!'); }, []); // 空数组,这个函数永不改变 return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(c => c + 1)}>Increment Count</button> {/* 当点击 Increment 时,App 重渲,但 MyButton 不会重渲 */} <MyButton onClick={handleClick} /> </div> ); }
自定义 Hooks - 逻辑复用的最佳实践
-
核心思想: 抽离和复用带状态的逻辑。可以看作是 Vue 的 Composable 函数。
-
关键认知:复用的是逻辑,不是状态 两个不同的组件同时调用
useMyHook()
,它们会得到两套完全独立、互不相干的内部状态。 -
实用技巧与示例:创建泛用的
useFetch
Hook 这是一个非常实用的自定义 Hook,封装了数据请求的完整逻辑,包括 loading、error 和请求取消。import { useState, useEffect } from "react"; function useFetch<T>( url: string, options?: RequestInit ): { data: T | null; loading: boolean; error: Error | null } { const [data, setData] = useState<T | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); useEffect(() => { // 每次请求前,重置状态 setLoading(true); setData(null); setError(null); const controller = new AbortController(); const signal = controller.signal; fetch(url, { ...options, signal }) .then((res) => { if (!res.ok) { throw new Error(`HTTP error! status: ${res.status}`); } return res.json(); }) .then((data) => { setData(data); }) .catch((err) => { if (err.name !== "AbortError") { setError(err); } }) .finally(() => { setLoading(false); }); // 清除函数:在组件卸载或 url 变化导致重新请求时,取消上一次的请求 return () => { controller.abort(); }; }, [url]); // 仅当 url 变化时重新执行 return { data, loading, error }; } // 如何在组件中使用 function UserProfile({ userId }) { const { data: user, loading, error, } = useFetch<{ name: string; email: string }>( `https://jsonplaceholder.typicode.com/users/${userId}` ); if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>; return ( <div> <h1>{user?.name}</h1> <p>{user?.email}</p> </div> ); }
实用技巧与最佳实践
1. 何时使用不同的 Hooks
- useState: 简单的组件状态管理
- useReducer: 复杂的状态逻辑,多种操作类型
- useContext: 跨组件共享数据,避免 prop drilling
- useMemo/useCallback: 性能优化,配合 React.memo 使用
2. 自定义 Hooks 的设计原则
- 单一职责: 每个 Hook 只处理一个特定的逻辑
- 可复用性: 设计通用的接口,避免硬编码
- 类型安全: 使用 TypeScript 提供完整的类型支持
// 好的自定义 Hook 设计
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue] as const;
}
// 使用示例
function UserSettings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [language, setLanguage] = useLocalStorage('language', 'zh-CN');
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-CN">中文</option>
<option value="en-US">English</option>
</select>
</div>
);
}
3. 性能优化的最佳实践
- 避免过早优化: 先确保功能正确,再考虑性能
- 使用 React DevTools Profiler: 识别真正的性能瓶颈
- 合理使用 React.memo: 配合 useCallback 和 useMemo 使用
// 性能优化的正确方式
import { memo, useCallback, useMemo } from 'react';
// 使用 memo 包装组件
const ExpensiveComponent = memo(({ data, onItemClick }) => {
// 使用 useMemo 缓存计算结果
const processedData = useMemo(() => {
return data.map(item => ({
...item,
processed: item.value * 2
}));
}, [data]);
return (
<div>
{processedData.map(item => (
<div key={item.id} onClick={() => onItemClick(item.id)}>
{item.name}: {item.processed}
</div>
))}
</div>
);
});
function ParentComponent() {
const [data, setData] = useState([]);
// 使用 useCallback 缓存函数
const handleItemClick = useCallback((id) => {
console.log('Item clicked:', id);
}, []);
return (
<ExpensiveComponent
data={data}
onItemClick={handleItemClick}
/>
);
}
本篇总结:
- 状态管理进阶: 使用
useReducer
处理复杂状态逻辑,useContext
解决跨组件数据共享 - 性能优化: 合理使用
useMemo
和useCallback
,避免不必要的重新渲染 - 逻辑复用: 通过自定义 Hooks 封装和复用复杂逻辑
- 最佳实践: 遵循单一职责、类型安全、性能优化的原则
掌握这些高级 Hooks,你就能构建出更加健壮、高效和可维护的 React 应用!