自定义Hook是一个JavaScript函数,其名称以"use"开头,函数内部可以调用其他的Hook。通过自定义Hook,可以将组件逻辑提取到可重用的函数中。
// ComponentA.js
function ComponentA() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(0);
return (
<div>
<p>计数: {count}</p>
<button onClick={increment}>增加</button>
<button onClick={decrement}>减少</button>
<button onClick={reset}>重置</button>
</div>
);
}
// ComponentB.js - 相同的逻辑重复了!
function ComponentB() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(0);
return (
<div>
<p>另一个计数: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>0</button>
</div>
);
}
// useCounter.js - 自定义Hook
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
// ComponentA.js - 使用自定义Hook
function ComponentA() {
const { count, increment, decrement, reset } = useCounter(0);
return (
<div>
<p>计数: {count}</p>
<button onClick={increment}>增加</button>
<button onClick={decrement}>减少</button>
<button onClick={reset}>重置</button>
</div>
);
}
// ComponentB.js - 复用同一个Hook
function ComponentB() {
const { count, increment, decrement, reset } = useCounter(10);
return (
<div>
<p>另一个计数: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>0</button>
</div>
);
}
use开头,这样React才能识别它为Hook并应用Hook规则检查。
| 命名方式 | 示例 | 是否推荐 |
|---|---|---|
use + 名词 |
useCounter, useTimer, useForm |
推荐 |
use + 动词 |
useFetch, useLocalStorage, useEventListener |
推荐 |
不以use开头 |
counterHook, customTimer |
错误 |
| 使用下划线 | use_counter |
不推荐 |
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('读取localStorage失败:', error);
return initialValue;
}
});
// 监听storedValue变化并更新localStorage
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error('写入localStorage失败:', error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
// 使用示例
function ThemeSwitcher() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return (
<div className={`app ${theme}`}>
<button onClick={toggleTheme}>
切换到{theme === 'light' ? '深色' : '浅色'}主题
</button>
</div>
);
}
import { useState, useEffect, useCallback } from 'react';
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, [url, options]);
useEffect(() => {
fetchData();
}, [fetchData]);
const refetch = () => {
fetchData();
};
return { data, loading, error, refetch };
}
// 使用示例
function UserProfile({ userId }) {
const { data: user, loading, error, refetch } = useFetch(
`https://api.example.com/users/${userId}`
);
if (loading) return <div>加载中...</div>;
if (error) return <div>错误: {error}</div>;
return (
<div>
<h2>{user.name}</h2>
<p>邮箱: {user.email}</p>
<button onClick={refetch}>刷新数据</button>
</div>
);
}
import { useState, useEffect } from 'react';
function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}
// 使用示例 - 搜索框防抖
function SearchBox() {
const [searchTerm, setSearchTerm] = useState('');
const [results, setResults] = useState([]);
const debouncedSearchTerm = useDebounce(searchTerm, 300);
useEffect(() => {
if (debouncedSearchTerm) {
// 执行搜索API调用
fetch(`/api/search?q=${debouncedSearchTerm}`)
.then(res => res.json())
.then(data => setResults(data));
} else {
setResults([]);
}
}, [debouncedSearchTerm]);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="输入搜索关键词..."
/>
<ul>
{results.map(result => (
<li key={result.id}>{result.title}</li>
))}
</ul>
</div>
);
}
import { useEffect, useRef } from 'react';
function useEventListener(eventName, handler, element = window) {
const savedHandler = useRef();
// 保存最新的handler到ref中
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
// 确保element支持addEventListener
const isSupported = element && element.addEventListener;
if (!isSupported) return;
const eventListener = (event) => {
if (savedHandler.current) {
savedHandler.current(event);
}
};
element.addEventListener(eventName, eventListener);
return () => {
element.removeEventListener(eventName, eventListener);
};
}, [eventName, element]);
}
// 使用示例 - 监听窗口大小变化
function WindowSizeDisplay() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEventListener('resize', () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
});
return (
<div>
窗口尺寸: {windowSize.width} x {windowSize.height}
</div>
);
}
import { useRef, useEffect } from 'react';
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// 使用示例 - 显示变化
function CounterWithPrevious() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>当前计数: {count}</p>
<p>上一次计数: {prevCount !== undefined ? prevCount : '无'}</p>
<button onClick={() => setCount(count + 1)}>
增加
</button>
</div>
);
}
import { useState, useEffect, useCallback } from 'react';
// 组合多个Hooks创建表单管理Hook
function useForm(initialValues = {}, validate = null) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
// 处理输入变化
const handleChange = useCallback((name, value) => {
setValues(prev => ({
...prev,
[name]: value
}));
// 标记字段为已触摸
setTouched(prev => ({
...prev,
[name]: true
}));
}, []);
// 处理表单提交
const handleSubmit = useCallback((onSubmit) => {
return async (event) => {
if (event) event.preventDefault();
// 验证表单
if (validate) {
const validationErrors = validate(values);
setErrors(validationErrors);
if (Object.keys(validationErrors).length > 0) {
return;
}
}
setIsSubmitting(true);
try {
await onSubmit(values);
} catch (error) {
console.error('表单提交错误:', error);
} finally {
setIsSubmitting(false);
}
};
}, [values, validate]);
// 重置表单
const resetForm = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
setIsSubmitting(false);
}, [initialValues]);
return {
values,
errors,
touched,
isSubmitting,
handleChange,
handleSubmit,
resetForm,
setValues
};
}
// 使用示例
function UserForm() {
const validate = (values) => {
const errors = {};
if (!values.name) errors.name = '姓名不能为空';
if (!values.email.includes('@')) errors.email = '邮箱格式不正确';
return errors;
};
const {
values,
errors,
touched,
isSubmitting,
handleChange,
handleSubmit
} = useForm({ name: '', email: '' }, validate);
const onSubmit = async (formValues) => {
console.log('提交表单:', formValues);
// 调用API等操作
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
type="text"
value={values.name}
onChange={(e) => handleChange('name', e.target.value)}
placeholder="姓名"
/>
{touched.name && errors.name && <span>{errors.name}</span>}
</div>
<div>
<input
type="email"
value={values.email}
onChange={(e) => handleChange('email', e.target.value)}
placeholder="邮箱"
/>
{touched.email && errors.email && <span>{errors.email}</span>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '提交中...' : '提交'}
</button>
</form>
);
}
import { renderHook, act } from '@testing-library/react-hooks';
import { useCounter } from './useCounter';
describe('useCounter', () => {
test('应该正确初始化计数', () => {
const { result } = renderHook(() => useCounter(5));
expect(result.current.count).toBe(5);
});
test('增加计数应该工作', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test('重置计数应该工作', () => {
const { result } = renderHook(() => useCounter(10));
act(() => {
result.current.increment();
result.current.reset();
});
expect(result.current.count).toBe(10);
});
});
// 测试useFetch
import { renderHook, waitFor } from '@testing-library/react';
import { useFetch } from './useFetch';
global.fetch = jest.fn();
describe('useFetch', () => {
beforeEach(() => {
fetch.mockClear();
});
test('应该正确加载数据', async () => {
const mockData = { id: 1, name: '测试' };
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockData,
});
const { result } = renderHook(() => useFetch('/api/test'));
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.data).toEqual(mockData);
expect(result.current.error).toBe(null);
});
});
阿里巴巴开源的React Hooks库,包含100+实用Hooks
Streamich的React Hooks集合,非常全面
数据获取、缓存和同步的Hook库
TypeScript友好的自定义Hooks集合
import { useState, useEffect } from 'react';
// 定义Hook返回类型
interface UseLocalStorageReturn<T> {
value: T;
setValue: (value: T) => void;
removeValue: () => void;
}
function useLocalStorage<T>(
key: string,
initialValue: T
): UseLocalStorageReturn<T> {
const [value, setValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
const removeValue = () => {
localStorage.removeItem(key);
setValue(initialValue);
};
return { value, setValue, removeValue };
}
// 使用示例 - 类型安全
interface UserPreferences {
theme: 'light' | 'dark';
language: string;
notifications: boolean;
}
function Preferences() {
const {
value: preferences,
setValue: setPreferences
} = useLocalStorage<UserPreferences>('preferences', {
theme: 'light',
language: 'zh-CN',
notifications: true
});
// TypeScript会检查类型
const toggleTheme = () => {
setPreferences({
...preferences,
theme: preferences.theme === 'light' ? 'dark' : 'light'
});
};
return (
<div>
<p>当前主题: {preferences.theme}</p>
<button onClick={toggleTheme}>切换主题</button>
</div>
);
}
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Hook在条件或循环中调用 | 违反Hook规则 | 确保Hook在顶层调用,不在条件、循环或嵌套函数中 |
| 无限重渲染循环 | useEffect依赖数组不正确 | 检查依赖项,使用useCallback/useMemo |
| Hook状态不同步 | 闭包问题 | 使用useRef保存可变值,或使用useReducer |
| 类型错误 | TypeScript类型定义不完善 | 完善泛型类型定义,使用类型断言 |
| 测试困难 | Hook依赖浏览器API | 使用renderHook测试,模拟浏览器API |