引言
在 React 开发中,Hooks 已经成为状态管理和逻辑复用的核心工具。除了 React 内置的 Hooks,自定义 Hooks 让我们能够将组件逻辑提取到可重用的函数中,实现更好的代码组织和复用。
今天我们来深入探讨自定义 Hooks 的封装技巧,并通过一个实用的 useLocalStorage Hook 来演示如何构建高质量的自定义 Hooks。
自定义 Hooks 的核心原则
1. 命名规范
自定义 Hooks 必须以 use 开头,这是 React 的硬性要求,也是代码可读性的保障:
// ✅ 正确
const useLocalStorage = (key, initialValue) => { ... }
const useFetch = (url) => { ... }
const useDebounce = (value, delay) => { ... }
// ❌ 错误
const localStorageHook = (key, initialValue) => { ... }
const fetchData = (url) => { ... }
2. 单一职责
每个自定义 Hooks 应该只负责一件事,保持逻辑清晰:
// ✅ 好的设计:每个 Hook 职责单一
const { user } = useAuth();
const { data } = useFetch('/api/user');
const { theme } = useTheme();
// ❌ 避免:一个 Hook 做太多事
const useEverything = () => {
// 认证 + 数据获取 + 主题管理...
}
3. 返回值设计
返回清晰的接口,优先使用对象解构:
// ✅ 清晰的返回值
const { value, setValue, removeValue } = useLocalStorage('key', 'default');
// ✅ 多个返回值时用数组
const [count, setCount] = useCounter(0);
实战:useLocalStorage Hook
基础实现
useLocalStorage 是最常用的自定义 Hooks 之一,它让本地存储变得简单优雅:
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// 读取初始值
const readValue = () => {
if (typeof window === 'undefined') {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
};
// 使用 lazy initialization
const [storedValue, setStoredValue] = useState(readValue);
// 监听其他标签页的变化
useEffect(() => {
const handleStorageChange = (event) => {
if (event.key === key && event.newValue !== null) {
try {
setStoredValue(JSON.parse(event.newValue));
} catch (error) {
console.warn('Error parsing storage event:', error);
}
}
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, [key]);
// 返回包装的 setter
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
// 触发当前标签页的事件
window.dispatchEvent(new Event('local-storage'));
}
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error);
}
};
const removeValue = () => {
try {
setStoredValue(initialValue);
if (typeof window !== 'undefined') {
window.localStorage.removeItem(key);
window.dispatchEvent(new Event('local-storage'));
}
} catch (error) {
console.warn(`Error removing localStorage key "${key}":`, error);
}
};
return [storedValue, setValue, removeValue];
}
export default useLocalStorage;
使用示例
import useLocalStorage from './hooks/useLocalStorage';
function UserProfile() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [user, setUser] = useLocalStorage('user', null);
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
const logout = () => {
setUser(null);
};
return (
<div className={`app ${theme}`}>
<button onClick={toggleTheme}>
切换到{theme === 'light' ? '深色' : '浅色'}模式
</button>
{user ? (
<div>
<p>欢迎,{user.name}!</p>
<button onClick={logout}>退出登录</button>
</div>
) : (
<button onClick={() => setUser({ name: '访客' })}>
模拟登录
</button>
)}
</div>
);
}
进阶:添加类型安全(TypeScript 版本)
import { useState, useEffect, Dispatch, SetStateAction } from 'react';
type SetValue<T> = Dispatch<SetStateAction<T>>;
function useLocalStorage<T>(key: string, initialValue: T): [T, SetValue<T>, () => void] {
const readValue = (): T => {
if (typeof window === 'undefined') {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
};
const [storedValue, setStoredValue] = useState<T>(readValue);
useEffect(() => {
const handleStorageChange = (event: StorageEvent) => {
if (event.key === key && event.newValue !== null) {
try {
setStoredValue(JSON.parse(event.newValue) as T);
} catch (error) {
console.warn('Error parsing storage event:', error);
}
}
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, [key]);
const setValue: SetValue<T> = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
window.dispatchEvent(new Event('local-storage'));
}
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error);
}
};
const removeValue = () => {
try {
setStoredValue(initialValue);
if (typeof window !== 'undefined') {
window.localStorage.removeItem(key);
window.dispatchEvent(new Event('local-storage'));
}
} catch (error) {
console.warn(`Error removing localStorage key "${key}":`, error);
}
};
return [storedValue, setValue, removeValue];
}
export default useLocalStorage;
封装技巧总结
- SSR 兼容:始终检查
window是否存在 - 错误处理:用 try-catch 包裹 localStorage 操作
- 事件同步:监听
storage事件实现多标签页同步 - 函数式更新:支持传入函数进行状态更新
- 类型安全:使用泛型提供完整的 TypeScript 支持
总结
自定义 Hooks 是 React 逻辑复用的强大工具。通过遵循命名规范、保持单一职责、设计清晰的返回值,我们可以构建出易于理解和维护的 Hooks。
useLocalStorage 作为一个经典案例,展示了如何处理浏览器 API、错误边界、跨标签页同步等实际问题。在下篇中,我们将继续探索 useFetch、useDebounce、useInterval 等更多实用的自定义 Hooks。