第 152 章:自定义 Hooks
学习目标
- 理解自定义 Hook 的规则
- 学会封装常用逻辑
- 掌握 5 个经典自定义 Hook
- 养成"逻辑抽 Hook"的习惯
一、自定义 Hook 是什么
以 use 开头的函数,内部可以调用其他 Hook,用于复用状态逻辑。
tsx
// 一个最简单的自定义 Hook
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue(v => !v), []);
return [value, toggle] as const;
}
// 使用
function MyComponent() {
const [open, toggleOpen] = useToggle(false);
return <button onClick={toggleOpen}>{open ? '关' : '开'}</button>;
}二、规则
2.1 命名以 use 开头
tsx
// ✅ 合法
function useLocalStorage() { }
function useFetch() { }
// ❌ 不算 Hook,不会做规则检查
function getData() { }2.2 只能在顶层调用
tsx
// ❌ 不要在条件里
function useBad() {
if (something) {
const [x] = useState(0); // 错
}
}
// ✅ 顶层
function useGood() {
const [x] = useState(0);
if (something) { /* 用 x */ }
}三、5 个经典自定义 Hook
3.1 useLocalStorage
tsx
function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
if (typeof window === 'undefined') return initial;
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initial;
} catch {
return initial;
}
});
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch {}
}, [key, value]);
return [value, setValue] as const;
}
// 使用
const [theme, setTheme] = useLocalStorage('theme', 'light');3.2 useFetch
tsx
interface FetchState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
function useFetch<T>(url: string) {
const [state, setState] = useState<FetchState<T>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
const controller = new AbortController();
setState({ data: null, loading: true, error: null });
fetch(url, { signal: controller.signal })
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
.then(data => setState({ data, loading: false, error: null }))
.catch(err => {
if (err.name === 'AbortError') return;
setState({ data: null, loading: false, error: err });
});
return () => controller.abort();
}, [url]);
return state;
}
// 使用
const { data, loading, error } = useFetch<User[]>('/api/users');3.3 useDebounce
tsx
function useDebounce<T>(value: T, delay = 300): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// 使用
const [query, setQuery] = useState('');
const debounced = useDebounce(query, 500);
useEffect(() => {
if (debounced) search(debounced);
}, [debounced]);3.4 usePrevious
tsx
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}3.5 useToggle
tsx
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse } as const;
}四、useOnline(网络状态)
tsx
function useOnline() {
const [online, setOnline] = useState(
typeof navigator !== 'undefined' ? navigator.onLine : true
);
useEffect(() => {
const onOnline = () => setOnline(true);
const onOffline = () => setOnline(false);
window.addEventListener('online', onOnline);
window.addEventListener('offline', onOffline);
return () => {
window.removeEventListener('online', onOnline);
window.removeEventListener('offline', onOffline);
};
}, []);
return online;
}五、useMediaQuery(响应式)
tsx
function useMediaQuery(query: string) {
const [matches, setMatches] = useState(false);
useEffect(() => {
const mq = window.matchMedia(query);
setMatches(mq.matches);
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, [query]);
return matches;
}
// 使用
const isMobile = useMediaQuery('(max-width: 768px)');六、组合 Hook
tsx
function useUser(id: number) {
const { data, loading, error } = useFetch<User>(`/api/users/${id}`);
const isAdmin = data?.role === 'admin';
return { user: data, loading, error, isAdmin };
}七、本章小结
| 概念 | 关键 |
|---|---|
| 自定义 Hook | use 开头的函数 |
| 用途 | 复用状态逻辑 |
| 规则 | 顶层调用、只在 React 中用 |
| 常见 | useLocalStorage / useFetch / useDebounce |
动手练习
- 写 useLocalStorage
- 写 useFetch(带 loading/error)
- 写 useDebounce + 搜索框
- 写 usePrevious
- 写 useScrollPosition(滚动监听)
推荐阅读
下一章:第 153 章:表单与受控组件 →