高级概念
React 18+
自定义 Hooks:复用逻辑
自定义 Hooks 让你能够提取和复用组件逻辑,而不是复用 UI
什么是自定义 Hooks?
自定义 Hook 是一个函数,名称以 "use" 开头,可以在内部调用其他 Hook。
两个组件之间的逻辑复用
TypeScript
import { useState, useEffect } from 'react';
// ❌ 重复的逻辑
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetchUser(userId).then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
if (loading) return <Spinner />;
return <div>{user.name}</div>;
}
function PostList({ postId }) {
const [post, setPost] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetchPost(postId).then(data => {
setPost(data);
setLoading(false);
});
}, [postId]);
if (loading) return <Spinner />;
return <div>{post.title}</div>;
}
提取自定义 Hook
TypeScript
// ✅ 自定义 Hook 复用逻辑
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(url)
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
});
}, [url]);
return { data, loading };
}
// 使用
function UserProfile({ userId }) {
const { data: user, loading } = useFetch(`/api/users/${userId}`);
if (loading) return <Spinner />;
return <div>{user.name}</div>;
}
function PostList({ postId }) {
const { data: post, loading } = useFetch(`/api/posts/${postId}`);
if (loading) return <Spinner />;
return <div>{post.title}</div>;
}
自定义 Hooks vs 普通函数
自定义 Hook 和普通函数的区别在于:
- 自定义 Hook 可以使用其他 Hook(useState, useEffect 等)
- 普通函数不能调用 Hook
- 自定义 Hook 必须以 "use" 开头
命名规范
以 "use" 开头
自定义 Hook 必须以 "use" 开头,这样 React 才能识别它为 Hook,并确保 Hooks 规则被正确应用。
TypeScript
// ✅ 正确的命名
function useData() {}
function useFetch() {}
function useLocalStorage() {}
function useWindowSize() {}
// ❌ 错误的命名
function getData() {}
function fetchData() {}
function getDataFromLocalStorage() {}
清晰的命名
TypeScript
// ✅ 好: 清晰描述功能
function useWindowSize() {}
function useLocalStorage(key, initialValue) {}
function useDebounce(callback, delay) {}
// ❌ 差: 不清晰的命名
function useHook() {}
function useStuff() {}
function useData() {}
常见自定义 Hooks 示例
1. useLocalStorage - 持久化状态
TypeScript
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// 从 localStorage 获取初始值
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
// 更新 localStorage
const setValue = (value) => {
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];
}
// 使用
function App() {
const [name, setName] = useLocalStorage('name', '');
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="输入你的名字"
/>
);
}
2. useWindowSize - 窗口尺寸
TypeScript
import { useState, useEffect } from 'react';
function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowSize;
}
// 使用
function ResponsiveComponent() {
const { width, height } = useWindowSize();
return (
<div>
窗口大小: {width} x {height}
</div>
);
}
3. useDebounce - 防抖
TypeScript
import { useState, useEffect } from 'react';
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
// 使用
function SearchInput() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
useEffect(() => {
// 使用防抖后的搜索词进行搜索
if (debouncedSearchTerm) {
console.log('搜索:', debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="搜索..."
/>
);
}
4. useToggle - 切换状态
TypeScript
import { useState, useCallback } from 'react';
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse, setValue };
}
// 使用
function Modal() {
const { value: isOpen, toggle, setFalse: close } = useToggle(false);
return (
<>
<button onClick={toggle}>打开模态框</button>
{isOpen && (
<div>
<p>这是一个模态框</p>
<button onClick={close}>关闭</button>
</div>
)}
</>
);
}
5. usePrevious - 保存之前的值
TypeScript
import { useEffect, useRef } from 'react';
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// 使用
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>当前: {count}</p>
<p>之前: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
);
}
6. useInterval - 动态定时器
TypeScript
import { useEffect, useRef } from 'react';
function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
// 使用
function Timer() {
const [seconds, setSeconds] = useState(0);
useInterval(() => {
setSeconds(seconds + 1);
}, 1000);
return <div>已运行: {seconds} 秒</div>;
}
7. useFetch - 数据获取
TypeScript
import { useState, useEffect, useCallback } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
const json = await response.json();
setData(json);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}, [url]);
useEffect(() => {
fetchData();
}, [fetchData]);
const refetch = useCallback(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch };
}
// 使用
function UserProfile({ userId }) {
const { data: user, loading, error, refetch } = useFetch(
`/api/users/${userId}`
);
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return (
<div>
<h1>{user.name}</h1>
<button onClick={refetch}>刷新</button>
</div>
);
}
8. useScript - 加载外部脚本
TypeScript
import { useState, useEffect } from 'react';
function useScript(src) {
const [status, setStatus] = useState('loading');
const [error, setError] = useState(null);
useEffect(() => {
let script = document.querySelector(`script[src="${src}"]`);
if (!script) {
script = document.createElement('script');
script.src = src;
script.async = true;
script.setAttribute('data-status', 'loading');
document.body.appendChild(script);
const setAttributeFromEvent = (event) => {
script.setAttribute(
'data-status',
event.type === 'load' ? 'ready' : 'error'
);
};
script.addEventListener('load', setAttributeFromEvent);
script.addEventListener('error', setAttributeFromEvent);
} else {
setStatus(script.getAttribute('data-status'));
}
const setStateFromEvent = (event) => {
if (event.type === 'load') {
setStatus('ready');
} else {
setStatus('error');
setError(new Error(`Failed to load script: ${src}`));
}
};
script.addEventListener('load', setStateFromEvent);
script.addEventListener('error', setStateFromEvent);
return () => {
script.removeEventListener('load', setStateFromEvent);
script.removeEventListener('error', setStateFromEvent);
};
}, [src]);
return { status, error };
}
// 使用
function GoogleMaps() {
const { status } = useScript(
'https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY'
);
if (status === 'loading') return <p>加载地图...</p>;
if (status === 'error') return <p>加载失败</p>;
return <div id="map" />;
}
组合自定义 Hooks
自定义 Hooks 可以互相调用,构建更复杂的逻辑。
TypeScript
// 基础 Hooks
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
// 组合 Hooks
function useWindowSizeDebounced(delay) {
const { width, height } = useWindowSize();
return {
width: useDebounce(width, delay),
height: useDebounce(height, delay),
};
}
// 使用
function ResponsiveComponent() {
const { width, height } = useWindowSizeDebounced(200);
return (
<div>
窗口大小(防抖): {width} x {height}
</div>
);
}
最佳实践
1. 单一职责
TypeScript
// ✅ 好: 每个 Hook 只做一件事
function useLocalStorage(key, initialValue) { /* ... */ }
function useFetch(url) { /* ... */ }
function useWindowSize() { /* ... */ }
// ❌ 差: 一个 Hook 做太多事情
function useApp() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [windowSize, setWindowSize] = useState({});
// 混合了太多逻辑...
}
2. 清晰的参数和返回值
TypeScript
// ✅ 好: 参数和返回值清晰
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// ...
return { data, loading, error, refetch };
}
// ❌ 差: 不清晰的返回值
function useFetch(url) {
// ...
return [data, loading, error, refetch]; // 数组返回,不清楚每个值的含义
}
3. 提供选项参数
TypeScript
// ✅ 好: 提供灵活的配置
function useFetch(url, options = {}) {
const {
method = 'GET',
headers = {},
body = null,
enabled = true,
} = options;
useEffect(() => {
if (!enabled) return;
fetch(url, { method, headers, body })
.then(res => res.json())
.then(setData);
}, [url, enabled, method, headers, body]);
return { data, loading, error };
}
4. 错误处理
TypeScript
// ✅ 好: 正确处理错误
function useFetch(url) {
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 response = await fetch(url, {
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
setData(json);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err);
}
} finally {
setLoading(false);
}
}
fetchData();
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
5. 类型安全 (TypeScript)
TypeScript
import { useState, useEffect } from 'react';
// 定义类型
type UseFetchResult<T> = {
data: T | null;
loading: boolean;
error: Error | null;
};
// 泛型 Hook
function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then((data: T) => {
setData(data);
setLoading(false);
})
.catch((err: Error) => {
setError(err);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
// 使用: 类型自动推断
interface User {
id: number;
name: string;
}
function UserProfile() {
const { data: user, loading, error } = useFetch<User>('/api/user');
if (loading) return <Spinner />;
if (error) return <Error />;
if (user) return <div>{user.name}</div>; // user 类型为 User | null
return null;
}
常见错误
1. 在条件语句中使用 Hook
TypeScript
// ❌ 错误: 在条件中使用
function useCustomHook(condition) {
if (condition) {
useState(0); // Error!
}
}
// ✅ 正确: 条件放在 Hook 内部
function useCustomHook(condition) {
const [value, setValue] = useState(0);
useEffect(() => {
if (condition) {
setValue(1);
}
}, [condition]);
return value;
}
2. 在普通函数中调用 Hook
TypeScript
// ❌ 错误:在普通函数中调用 Hook
function getData() {
const [data, setData] = useState(null); // Error!
return data;
}
// ✅ 正确:创建自定义 Hook
function useData() {
const [data, setData] = useState(null);
return data;
}
3. 循环中使用 Hook
TypeScript
// ❌ 错误:在循环中调用 Hook
function useItems(items) {
items.forEach(item => {
useEffect(() => { // Error!
console.log(item);
}, [item]);
});
}
// ✅ 正确:使用单个 effect 处理所有 items
function useItems(items) {
useEffect(() => {
items.forEach(item => {
console.log(item);
});
}, [items]);
}
测试自定义 Hooks
使用 @testing-library/react-hooks 测试自定义 Hooks。
TypeScript
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
test('should increment counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test('should decrement counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(-1);
});
测试最佳实践
- 测试 Hook 的状态变化
- 测试副作用是否正确执行
- 测试错误处理
- 测试依赖项变化时的行为
常用的第三方 Hooks 库
- ahooks - React Hooks 库(中文社区) (https://ahooks.js.org/)
- react-use - 常用 React Hooks 集合 (https://github.com/streamich/react-use)
- usehooks-ts - TypeScript React Hooks (https://usehooks-ts.com/)
- rooks - React Hooks 集合 (https://rooks.vercel.app/)
下一步
现在你已经了解了自定义 Hooks,可以继续学习:
这篇文章有帮助吗?
Previous / Next
Related Links