React 自定义 Hook
概述
自定义 Hook 是 React 提供的一种复用状态逻辑的方式。通过创建自定义 Hook,我们可以将组件逻辑提取到可复用的函数中,实现代码的模块化和复用。本章将学习如何创建和使用自定义 Hook。
🎣 基础自定义 Hook
简单状态管理 Hook
jsx
// 自定义 Hook:计数器
function useCounter(initialValue = 0) {
const [count, setCount] = React.useState(initialValue);
const increment = React.useCallback(() => {
setCount(prev => prev + 1);
}, []);
const decrement = React.useCallback(() => {
setCount(prev => prev - 1);
}, []);
const reset = React.useCallback(() => {
setCount(initialValue);
}, [initialValue]);
return {
count,
increment,
decrement,
reset,
setCount
};
}
// 使用自定义 Hook
function CounterExample() {
const counter1 = useCounter(0);
const counter2 = useCounter(10);
return (
<div style={{ padding: '20px' }}>
<h2>自定义计数器 Hook</h2>
<div style={{ marginBottom: '20px' }}>
<h3>计数器 1</h3>
<p>当前值: {counter1.count}</p>
<button onClick={counter1.increment}>+1</button>
<button onClick={counter1.decrement}>-1</button>
<button onClick={counter1.reset}>重置</button>
</div>
<div>
<h3>计数器 2 (初始值: 10)</h3>
<p>当前值: {counter2.count}</p>
<button onClick={counter2.increment}>+1</button>
<button onClick={counter2.decrement}>-1</button>
<button onClick={counter2.reset}>重置</button>
</div>
</div>
);
}切换状态 Hook
jsx
// 自定义 Hook:布尔值切换
function useToggle(initialValue = false) {
const [value, setValue] = React.useState(initialValue);
const toggle = React.useCallback(() => {
setValue(prev => !prev);
}, []);
const setTrue = React.useCallback(() => {
setValue(true);
}, []);
const setFalse = React.useCallback(() => {
setValue(false);
}, []);
return {
value,
toggle,
setTrue,
setFalse,
setValue
};
}
// 使用示例
function ToggleExample() {
const modal = useToggle(false);
const sidebar = useToggle(true);
const darkMode = useToggle(false);
return (
<div style={{
padding: '20px',
backgroundColor: darkMode.value ? '#333' : '#fff',
color: darkMode.value ? '#fff' : '#333',
minHeight: '400px'
}}>
<h2>切换状态演示</h2>
<div style={{ marginBottom: '20px' }}>
<button onClick={modal.toggle}>
{modal.value ? '关闭' : '打开'}模态框
</button>
<button onClick={sidebar.toggle} style={{ marginLeft: '10px' }}>
{sidebar.value ? '隐藏' : '显示'}侧边栏
</button>
<button onClick={darkMode.toggle} style={{ marginLeft: '10px' }}>
{darkMode.value ? '浅色' : '深色'}模式
</button>
</div>
{sidebar.value && (
<div style={{
position: 'fixed',
left: 0,
top: 0,
width: '200px',
height: '100vh',
backgroundColor: darkMode.value ? '#444' : '#f8f9fa',
padding: '20px',
borderRight: '1px solid #ddd'
}}>
<h3>侧边栏</h3>
<ul>
<li>导航项 1</li>
<li>导航项 2</li>
<li>导航项 3</li>
</ul>
</div>
)}
{modal.value && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000
}}>
<div style={{
backgroundColor: darkMode.value ? '#444' : 'white',
padding: '30px',
borderRadius: '8px',
maxWidth: '400px',
width: '90%'
}}>
<h3>模态框</h3>
<p>这是一个使用自定义 Hook 控制的模态框。</p>
<button onClick={modal.setFalse}>关闭</button>
</div>
</div>
)}
</div>
);
}🌐 网络请求 Hook
基础数据获取 Hook
jsx
// 自定义 Hook:数据获取
function useFetch(url, options = {}) {
const [data, setData] = React.useState(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
const fetchData = React.useCallback(async () => {
try {
setLoading(true);
setError(null);
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 1000));
// 模拟 API 响应
const mockData = {
'/api/users': [
{ id: 1, name: '张三', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', email: 'lisi@example.com' }
],
'/api/posts': [
{ id: 1, title: '第一篇文章', content: '这是第一篇文章的内容' },
{ id: 2, title: '第二篇文章', content: '这是第二篇文章的内容' }
]
};
if (Math.random() > 0.8) {
throw new Error('网络请求失败');
}
setData(mockData[url] || { message: '数据获取成功' });
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, [url]);
React.useEffect(() => {
if (url) {
fetchData();
}
}, [url, fetchData]);
const refetch = React.useCallback(() => {
fetchData();
}, [fetchData]);
return {
data,
loading,
error,
refetch
};
}
// 使用数据获取 Hook
function DataFetchingExample() {
const [endpoint, setEndpoint] = React.useState('/api/users');
const { data, loading, error, refetch } = useFetch(endpoint);
return (
<div style={{ padding: '20px' }}>
<h2>数据获取演示</h2>
<div style={{ marginBottom: '20px' }}>
<label>选择接口: </label>
<select value={endpoint} onChange={(e) => setEndpoint(e.target.value)}>
<option value="/api/users">用户列表</option>
<option value="/api/posts">文章列表</option>
<option value="/api/unknown">未知接口</option>
</select>
<button onClick={refetch} style={{ marginLeft: '10px' }}>
重新获取
</button>
</div>
{loading && (
<div style={{ textAlign: 'center', padding: '40px' }}>
⏳ 加载中...
</div>
)}
{error && (
<div style={{
backgroundColor: '#f8d7da',
color: '#721c24',
padding: '15px',
borderRadius: '4px',
border: '1px solid #f5c6cb'
}}>
❌ 错误: {error}
</div>
)}
{data && !loading && (
<div style={{
backgroundColor: '#d4edda',
color: '#155724',
padding: '15px',
borderRadius: '4px',
border: '1px solid #c3e6cb'
}}>
<h3>数据获取成功</h3>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)}
</div>
);
}高级异步 Hook
jsx
// 自定义 Hook:异步操作
function useAsync(asyncFunction, immediate = true) {
const [status, setStatus] = React.useState('idle');
const [value, setValue] = React.useState(null);
const [error, setError] = React.useState(null);
const execute = React.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]);
React.useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return {
execute,
status,
value,
error,
isPending: status === 'pending',
isSuccess: status === 'success',
isError: status === 'error',
isIdle: status === 'idle'
};
}
// 模拟异步函数
const mockApiCall = async (type) => {
await new Promise(resolve => setTimeout(resolve, 2000));
if (type === 'error') {
throw new Error('模拟的错误');
}
return {
type,
data: `${type} 的数据`,
timestamp: new Date().toISOString()
};
};
// 使用异步 Hook
function AsyncExample() {
const api1 = useAsync(() => mockApiCall('success'), false);
const api2 = useAsync(() => mockApiCall('error'), false);
return (
<div style={{ padding: '20px' }}>
<h2>异步操作演示</h2>
<div style={{ display: 'flex', gap: '20px' }}>
<div style={{ flex: 1, border: '1px solid #ddd', padding: '15px', borderRadius: '8px' }}>
<h3>成功场景</h3>
<button
onClick={() => api1.execute('success')}
disabled={api1.isPending}
>
{api1.isPending ? '请求中...' : '发起请求'}
</button>
{api1.isPending && <div>⏳ 请求中...</div>}
{api1.isSuccess && (
<div style={{ color: 'green', marginTop: '10px' }}>
✅ 成功: {JSON.stringify(api1.value, null, 2)}
</div>
)}
{api1.isError && (
<div style={{ color: 'red', marginTop: '10px' }}>
❌ 错误: {api1.error.message}
</div>
)}
</div>
<div style={{ flex: 1, border: '1px solid #ddd', padding: '15px', borderRadius: '8px' }}>
<h3>错误场景</h3>
<button
onClick={() => api2.execute('error')}
disabled={api2.isPending}
>
{api2.isPending ? '请求中...' : '发起请求(会失败)'}
</button>
{api2.isPending && <div>⏳ 请求中...</div>}
{api2.isSuccess && (
<div style={{ color: 'green', marginTop: '10px' }}>
✅ 成功: {JSON.stringify(api2.value, null, 2)}
</div>
)}
{api2.isError && (
<div style={{ color: 'red', marginTop: '10px' }}>
❌ 错误: {api2.error.message}
</div>
)}
</div>
</div>
</div>
);
}💾 本地存储 Hook
localStorage Hook
jsx
// 自定义 Hook:本地存储
function useLocalStorage(key, initialValue) {
// 获取初始值
const [storedValue, setStoredValue] = React.useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error('Error reading localStorage:', error);
return initialValue;
}
});
// 设置值
const setValue = React.useCallback((value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error('Error setting localStorage:', error);
}
}, [key, storedValue]);
// 删除值
const removeValue = React.useCallback(() => {
try {
window.localStorage.removeItem(key);
setStoredValue(initialValue);
} catch (error) {
console.error('Error removing localStorage:', error);
}
}, [key, initialValue]);
return [storedValue, setValue, removeValue];
}
// 使用本地存储 Hook
function LocalStorageExample() {
const [name, setName, removeName] = useLocalStorage('user-name', '');
const [settings, setSettings, removeSettings] = useLocalStorage('user-settings', {
theme: 'light',
notifications: true,
language: 'zh-CN'
});
const [todos, setTodos, removeTodos] = useLocalStorage('todos', []);
const addTodo = () => {
const newTodo = {
id: Date.now(),
text: `待办事项 ${todos.length + 1}`,
completed: false
};
setTodos([...todos, newTodo]);
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const updateSettings = (key, value) => {
setSettings(prev => ({ ...prev, [key]: value }));
};
return (
<div style={{
padding: '20px',
backgroundColor: settings.theme === 'dark' ? '#333' : '#fff',
color: settings.theme === 'dark' ? '#fff' : '#333',
minHeight: '100vh'
}}>
<h2>本地存储演示</h2>
{/* 用户名设置 */}
<div style={{ marginBottom: '20px' }}>
<h3>用户名</h3>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="输入用户名"
style={{
padding: '8px',
marginRight: '10px',
backgroundColor: settings.theme === 'dark' ? '#444' : '#fff',
color: settings.theme === 'dark' ? '#fff' : '#333',
border: '1px solid #ddd'
}}
/>
<button onClick={removeName}>清除</button>
{name && <p>欢迎,{name}!</p>}
</div>
{/* 设置 */}
<div style={{ marginBottom: '20px' }}>
<h3>设置</h3>
<div style={{ marginBottom: '10px' }}>
<label>
主题:
<select
value={settings.theme}
onChange={(e) => updateSettings('theme', e.target.value)}
style={{ marginLeft: '10px' }}
>
<option value="light">浅色</option>
<option value="dark">深色</option>
</select>
</label>
</div>
<div style={{ marginBottom: '10px' }}>
<label>
<input
type="checkbox"
checked={settings.notifications}
onChange={(e) => updateSettings('notifications', e.target.checked)}
/>
启用通知
</label>
</div>
<div style={{ marginBottom: '10px' }}>
<label>
语言:
<select
value={settings.language}
onChange={(e) => updateSettings('language', e.target.value)}
style={{ marginLeft: '10px' }}
>
<option value="zh-CN">中文</option>
<option value="en-US">English</option>
</select>
</label>
</div>
<button onClick={removeSettings}>重置设置</button>
</div>
{/* 待办事项 */}
<div>
<h3>待办事项 (自动保存)</h3>
<button onClick={addTodo} style={{ marginBottom: '10px' }}>
添加待办事项
</button>
<div>
{todos.map(todo => (
<div
key={todo.id}
style={{
padding: '8px',
margin: '4px 0',
backgroundColor: settings.theme === 'dark' ? '#444' : '#f8f9fa',
borderRadius: '4px'
}}
>
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none',
marginLeft: '8px'
}}>
{todo.text}
</span>
</label>
</div>
))}
</div>
{todos.length > 0 && (
<button onClick={removeTodos} style={{ marginTop: '10px' }}>
清除所有待办事项
</button>
)}
</div>
</div>
);
}⚡ 性能优化 Hook
防抖 Hook
jsx
// 自定义 Hook:防抖
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = React.useState(value);
React.useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// 自定义 Hook:防抖回调
function useDebouncedCallback(callback, delay) {
const timeoutRef = React.useRef(null);
const debouncedCallback = React.useCallback((...args) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
callback(...args);
}, delay);
}, [callback, delay]);
React.useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return debouncedCallback;
}
// 防抖演示
function DebounceExample() {
const [searchTerm, setSearchTerm] = React.useState('');
const [searchResults, setSearchResults] = React.useState([]);
const [searching, setSearching] = React.useState(false);
// 防抖搜索词
const debouncedSearchTerm = useDebounce(searchTerm, 500);
// 防抖搜索函数
const debouncedSearch = useDebouncedCallback((term) => {
if (term) {
setSearching(true);
// 模拟搜索 API
setTimeout(() => {
const mockResults = [
`${term} 的结果 1`,
`${term} 的结果 2`,
`${term} 的结果 3`
];
setSearchResults(mockResults);
setSearching(false);
}, 300);
} else {
setSearchResults([]);
}
}, 300);
// 使用防抖值触发搜索
React.useEffect(() => {
debouncedSearch(debouncedSearchTerm);
}, [debouncedSearchTerm, debouncedSearch]);
return (
<div style={{ padding: '20px' }}>
<h2>防抖搜索演示</h2>
<div style={{ marginBottom: '20px' }}>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="输入搜索内容..."
style={{
padding: '10px',
width: '300px',
border: '1px solid #ddd',
borderRadius: '4px'
}}
/>
<div style={{ fontSize: '12px', color: '#666', marginTop: '5px' }}>
输入会在停止 500ms 后触发搜索
</div>
</div>
{searching && (
<div style={{ color: '#007bff' }}>
🔍 搜索中...
</div>
)}
{searchResults.length > 0 && (
<div>
<h3>搜索结果:</h3>
<ul>
{searchResults.map((result, index) => (
<li key={index}>{result}</li>
))}
</ul>
</div>
)}
<div style={{ marginTop: '30px', fontSize: '14px', backgroundColor: '#f8f9fa', padding: '15px', borderRadius: '8px' }}>
<h4>实时状态:</h4>
<p><strong>输入值:</strong> "{searchTerm}"</p>
<p><strong>防抖值:</strong> "{debouncedSearchTerm}"</p>
<p><strong>搜索状态:</strong> {searching ? '搜索中' : '空闲'}</p>
</div>
</div>
);
}📝 本章小结
通过本章学习,你应该掌握了:
自定义 Hook 核心概念
- ✅ Hook 的命名规范(use 开头)
- ✅ 状态逻辑的提取和复用
- ✅ 副作用的封装和管理
- ✅ 性能优化技巧
常用 Hook 模式
- ✅ 状态管理:useCounter、useToggle
- ✅ 数据获取:useFetch、useAsync
- ✅ 本地存储:useLocalStorage
- ✅ 性能优化:useDebounce、useDebouncedCallback
最佳实践
- 单一职责:每个 Hook 只处理一种逻辑
- 依赖优化:合理使用 useCallback 和 useMemo
- 错误处理:包含完整的错误处理逻辑
- 清理资源:及时清理副作用和订阅
- 类型安全:使用 TypeScript 增强类型安全
继续学习:下一章 - React 样式处理