创作者: Yardon | GitHub: github.com/YardonYan | 版本: v1.0
为什么需要自定义 Hook
假设你在三个不同的页面都要做一个功能:用户输入搜索词后,等 300ms 没动静了才发请求(防抖)。
你可以在每个页面都写一遍:
jsx
function SearchPage1() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
const timeout = setTimeout(() => {
if (query) fetchResults(query).then(setResults);
}, 300);
return () => clearTimeout(timeout);
}, [query]);
// ... 这段代码要复制三份!
}
这就是典型的代码重复。防抖逻辑本身是独立的,完全可以抽出来。而且当产品说"改成 500ms"时,你只要改一个地方。
自定义 Hook 就是为这个场景设计的------把可复用的逻辑封装成函数,这个函数内部可以用其他 Hook。
自定义 Hook 的基本结构
自定义 Hook 本质上就是一个普通的 JavaScript 函数,函数名以 use 开头,内部可以调用其他 Hook。
javascript
function useDebounce(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timeout = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timeout);
}, [value, delay]);
return debouncedValue;
}
这就是一个 Hook。它用到了 useState 和 useEffect,所以它自己也是个 Hook(React 的规则:只有 Hook 才能调用其他 Hook)。
用法
jsx
function SearchPage() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) fetchResults(debouncedQuery);
}, [debouncedQuery]);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
现在防抖逻辑只在一处定义了,三个页面都可以用。
经典案例:useDebounce
继续深化这个案例,加上更多防抖的变体:
javascript
function useDebouncedValue(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// 防抖版表单:用户停止输入后才更新
function useDebouncedForm(initialValues, delay = 300) {
const [values, setValues] = useState(initialValues);
// 为每个字段单独防抖
const debouncedValues = {};
for (const key in values) {
debouncedValues[key] = useDebouncedValue(values[key], delay);
}
function handleChange(key, value) {
setValues((prev) => ({ ...prev, [key]: value }));
}
return { values, debouncedValues, handleChange };
}
经典案例:useFetch
数据获取是另一个重复高频的��景:
javascript
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
setLoading(true);
setError(null);
const res = await fetch(url, {
...options,
signal: controller.signal
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
setData(json);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
}
fetchData();
return () => controller.abort();
}, [url, JSON.stringify(options)]);
return { data, loading, error, refetch: () => /* ... */ };
}
用法变得极其简单
jsx
function UserList() {
const { data, loading, error } = useFetch('/api/users');
if (loading) return <Spinner />;
if (error) return <Error msg={error} />;
return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
一行代码替代了 30 行重复的请求/加载/错误逻辑。这就是 Hook 的价值。
经典案例:useLocalStorage
把数据存进浏览器本地存储,同时保持和 React 状态的同步:
javascript
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.warn(`读取 localStorage key "${key}" 失败:`, error);
return initialValue;
}
});
const setValue = useCallback((value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.warn(`写入 localStorage key "${key}" 失败:`, error);
}
}, [key, storedValue]);
return [storedValue, setValue];
}
用法:记住用户的偏好
jsx
function App() {
const [theme, setTheme] = useLocalStorage('theme', 'dark');
const [language, setLanguage] = useLocalStorage('language', 'zh-CN');
// 用户刷新页面后,主题和语言自动恢复
return <ThemeProvider theme={theme}>...</ThemeProvider>;
}
Hook 组合:更复杂的逻辑
你可以把多个自定义 Hook 组合在一起,形成更强大的逻辑:
javascript
// 一个组合 Hook:用户搜索 + 防抖 + 缓存
function useSearch(query, options = {}) {
const { baseUrl = '/api/search', delay = 300, cache = true } = options;
const debouncedQuery = useDebounce(query, delay);
const cacheKey = `${baseUrl}:${debouncedQuery}`;
const [cachedData, setCachedData] = useLocalStorage(`search-cache`, {});
const [freshData, setFreshData] = useState(null);
// 优先用缓存
const data = cache && cachedData[cacheKey] ? cachedData[cacheKey] : freshData;
useEffect(() => {
if (!debouncedQuery) return;
// 检查缓存
const cached = cachedData[cacheKey];
if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) {
// 5 分钟内的缓存直接用
return;
}
// 发新请求
fetch(`${baseUrl}?q=${encodeURIComponent(debouncedQuery)}`)
.then((r) => r.json())
.then((d) => {
setFreshData(d);
// 更新缓存
setCachedData((prev) => ({
...prev,
[cacheKey]: { ...d, timestamp: Date.now() }
}));
});
}, [debouncedQuery, baseUrl]);
return { data, isLoading: !debouncedQuery || !data };
}
这就是所谓的「管道式」架构------每个 Hook 只做一件事,组合起来就拥有了完整功能。
测试自定义 Hook
自定义 Hook 的测试需要一点特殊处理------React Testing Library 专门为 Hook 提供了 renderHook:
javascript
import { renderHook, act } from '@testing-library/react';
test('useDebounce 应该延迟返回新值', () => {
const { result, rerender } = renderHook(
({ value, delay }) => useDebounce(value, delay),
{ initialProps: { value: 'hello', delay: 300 } }
);
expect(result.current).toBe('hello');
// 修改值
rerender({ value: 'world', delay: 300 });
expect(result.current).toBe('hello'); // 还没到 300ms
// 等 300ms
jest.advanceTimersByTime(300);
expect(result.current).toBe('world');
});
本章小结
| 概念 | 一句话总结 |
|---|---|
| 自定义 Hook | 以 use 开头的函数,内部可调用其他 Hook |
| useDebounce | 延迟更新值,常用于搜索输入 |
| useFetch | 封装请求逻辑,一行代码搞定数据获取 |
| useLocalStorage | 持久化状态到浏览器本地存储 |
| Hook 组合 | 把多个简单 Hook 组合成复杂逻辑 |
自定义 Hook 把可复用的逻辑抽离出来------这是 React 应用架构的核心技能。下一章我们聊 状态管理------当组件树越来越深时,怎么让状态在任意位置都能访问。
📌 创作者: Yardon | 🏠 个人网站: GlimmerAI.top
📖 本章是「React 从入门到生产 」系列的第 4 章。上一章:副作用与数据获取 | 下一章:状态管理选型
🌟 如果你觉得有帮助,欢迎访问 GlimmerAI.top 查看我的更多作品。欢迎大家来观看!