概念意义与作用
什么是自定义Hook?
自定义Hook是React 16.8引入Hooks特性后的重要扩展能力,它允许开发者将组件逻辑提取到可重用的函数中。不同于传统的HOC(高阶组件)或Render Props模式,自定义Hook提供了一种更自然、更符合函数式编程思维的逻辑复用方式。
核心价值与意义
- 逻辑复用性:将通用业务逻辑封装成Hook,实现跨组件、跨项目的代码复用
- 关注点分离:将复杂组件的状态逻辑与UI渲染分离,提升代码可读性
- 组合性:多个自定义Hook可以组合使用,构建复杂功能
- 测试友好:纯函数特性使得逻辑测试更加简单
- 类型安全:配合TypeScript,提供完整的类型推导
代码实例解析
1. 数据请求Hook封装
import { useState, useEffect, useCallback } from 'react';
// 基础数据请求Hook
export function useFetch<T>(url: string, options?: RequestInit) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
}, [url, options]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch: fetchData };
}
// 增强版 - 带缓存和依赖管理的数据请求Hook
export function useQuery<T>(
key: string | string[],
fetcher: () => Promise<T>,
options?: {
enabled?: boolean;
staleTime?: number;
cacheTime?: number;
}
) {
const [state, setState] = useState<{
data: T | null;
loading: boolean;
error: Error | null;
}>({
data: null,
loading: true,
error: null,
});
const cacheKey = Array.isArray(key) ? key.join('-') : key;
useEffect(() => {
if (options?.enabled === false) return;
const executeQuery = async () => {
try {
setState(prev => ({ ...prev, loading: true, error: null }));
// 简单的缓存实现
const cached = sessionStorage.getItem(cacheKey);
if (cached) {
const cachedData = JSON.parse(cached);
setState(prev => ({ ...prev, data: cachedData, loading: false }));
}
const result = await fetcher();
// 缓存结果
sessionStorage.setItem(cacheKey, JSON.stringify(result));
setState(prev => ({ ...prev, data: result, loading: false }));
} catch (err) {
setState(prev => ({ ...prev, error: err as Error, loading: false }));
}
};
executeQuery();
}, [cacheKey, fetcher, options?.enabled]);
return state;
}
// 使用示例
function UserProfile({ userId }: { userId: string }) {
const { data: user, loading, error } = useQuery(
['user', userId],
() => fetch(`/api/users/${userId}`).then(res => res.json()),
{ enabled: !!userId }
);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{user?.name}</h1>
<p>{user?.email}</p>
</div>
);
}
2. 表单管理Hook
import { useState, useCallback } from 'react';
export function useForm<T extends Record<string, any>>(
initialValues: T,
validators?: Partial<Record<keyof T, (value: any) => string | undefined>>
) {
const [values, setValues] = useState<T>(initialValues);
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
const setValue = useCallback((name: keyof T, value: any) => {
setValues(prev => ({ ...prev, [name]: value }));
// 实时验证
if (validators?.[name]) {
const error = validators[name]!(value);
setErrors(prev => ({
...prev,
[name]: error
}));
}
}, [validators]);
const handleChange = useCallback((
name: keyof T,
value?: any
) => {
if (arguments.length === 1) {
// 处理事件对象
return (e: React.ChangeEvent<HTMLInputElement>) => {
const { value, type, checked } = e.target;
setValue(name, type === 'checkbox' ? checked : value);
};
}
setValue(name, value);
}, [setValue]);
const handleBlur = useCallback((name: keyof T) => {
setTouched(prev => ({ ...prev, [name]: true }));
}, []);
const validate = useCallback(() => {
const newErrors: Partial<Record<keyof T, string>> = {};
if (validators) {
Object.keys(validators).forEach(key => {
const validator = validators[key as keyof T];
if (validator) {
const error = validator(values[key as keyof T]);
if (error) {
newErrors[key as keyof T] = error;
}
}
});
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
}, [values, validators]);
const reset = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
}, [initialValues]);
return {
values,
errors,
touched,
handleChange,
handleBlur,
setValue,
validate,
reset,
isValid: Object.keys(errors).length === 0
};
}
// 使用示例
function LoginForm() {
const { values, errors, touched, handleChange, handleBlur, validate } = useForm(
{ email: '', password: '' },
{
email: (value) => !value ? 'Email is required' : !/\S+@\S+\.\S+/.test(value) ? 'Invalid email' : undefined,
password: (value) => !value ? 'Password is required' : value.length < 6 ? 'Password too short' : undefined
}
);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validate()) {
console.log('Form submitted:', values);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="email"
name="email"
value={values.email}
onChange={handleChange('email')}
onBlur={() => handleBlur('email')}
placeholder="Email"
/>
{touched.email && errors.email && <span>{errors.email}</span>}
</div>
<div>
<input
type="password"
name="password"
value={values.password}
onChange={handleChange('password')}
onBlur={() => handleBlur('password')}
placeholder="Password"
/>
{touched.password && errors.password && <span>{errors.password}</span>}
</div>
<button type="submit">Login</button>
</form>
);
}
3. 拖拽功能Hook
import { useState, useRef, useCallback, useEffect } from 'react';
interface DragOptions {
onDragStart?: (event: React.DragEvent) => void;
onDrag?: (event: React.DragEvent, delta: { x: number; y: number }) => void;
onDragEnd?: (event: React.DragEvent) => void;
}
export function useDrag(options: DragOptions = {}) {
const [isDragging, setIsDragging] = useState(false);
const dragStartPos = useRef({ x: 0, y: 0 });
const elementRef = useRef<HTMLElement>(null);
const handleDragStart = useCallback((e: React.DragEvent) => {
setIsDragging(true);
dragStartPos.current = { x: e.clientX, y: e.clientY };
// 设置拖拽图像
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'move';
// 创建透明图像作为拖拽反馈
const dragImage = document.createElement('div');
dragImage.style.opacity = '0';
document.body.appendChild(dragImage);
e.dataTransfer.setDragImage(dragImage, 0, 0);
setTimeout(() => document.body.removeChild(dragImage), 0);
}
options.onDragStart?.(e);
}, [options]);
const handleDrag = useCallback((e: React.DragEvent) => {
if (!isDragging) return;
const delta = {
x: e.clientX - dragStartPos.current.x,
y: e.clientY - dragStartPos.current.y
};
options.onDrag?.(e, delta);
}, [isDragging, options]);
const handleDragEnd = useCallback((e: React.DragEvent) => {
setIsDragging(false);
options.onDragEnd?.(e);
}, [options]);
// 绑定事件到元素
useEffect(() => {
const element = elementRef.current;
if (!element) return;
const handleMouseDown = (e: MouseEvent) => {
const dragEvent = e as unknown as React.DragEvent;
handleDragStart(dragEvent);
};
element.addEventListener('mousedown', handleMouseDown);
return () => element.removeEventListener('mousedown', handleMouseDown);
}, [handleDragStart]);
return {
isDragging,
elementRef,
dragHandlers: {
onDragStart: handleDragStart,
onDrag: handleDrag,
onDragEnd: handleDragEnd,
}
};
}
// 高级拖拽Hook - 支持边界限制和网格对齐
export function useAdvancedDrag(
bounds?: { minX?: number; maxX?: number; minY?: number; maxY?: number },
gridSize: number = 1
) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const isDraggingRef = useRef(false);
const { elementRef, dragHandlers } = useDrag({
onDragStart: () => {
isDraggingRef.current = true;
},
onDrag: (e, delta) => {
if (!isDraggingRef.current) return;
let newX = Math.round(delta.x / gridSize) * gridSize;
let newY = Math.round(delta.y / gridSize) * gridSize;
// 边界检查
if (bounds) {
if (bounds.minX !== undefined) newX = Math.max(bounds.minX, newX);
if (bounds.maxX !== undefined) newX = Math.min(bounds.maxX, newX);
if (bounds.minY !== undefined) newY = Math.max(bounds.minY, newY);
if (bounds.maxY !== undefined) newY = Math.min(bounds.maxY, newY);
}
setPosition({ x: newX, y: newY });
},
onDragEnd: () => {
isDraggingRef.current = false;
}
});
return {
position,
elementRef,
dragHandlers,
setPosition
};
}
// 使用示例 - 可拖拽组件
function DraggableItem() {
const { position, elementRef, dragHandlers } = useAdvancedDrag(
{ minX: 0, maxX: 500, minY: 0, maxY: 500 },
10 // 10px网格对齐
);
return (
<div
ref={elementRef as React.RefObject<HTMLDivElement>}
{...dragHandlers}
style={{
position: 'absolute',
left: position.x,
top: position.y,
width: 100,
height: 100,
backgroundColor: 'lightblue',
cursor: 'move',
userSelect: 'none',
border: '1px solid #ccc'
}}
>
拖拽我
</div>
);
}
// 使用示例 - 拖拽列表
function DraggableList() {
const [items, setItems] = useState(['Item 1', 'Item 2', 'Item 3']);
const handleDragStart = (index: number) => (e: React.DragEvent) => {
e.dataTransfer.setData('text/plain', index.toString());
};
const handleDrop = (targetIndex: number) => (e: React.DragEvent) => {
e.preventDefault();
const sourceIndex = parseInt(e.dataTransfer.getData('text/plain'));
if (sourceIndex !== targetIndex) {
const newItems = [...items];
const [movedItem] = newItems.splice(sourceIndex, 1);
newItems.splice(targetIndex, 0, movedItem);
setItems(newItems);
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
};
return (
<div>
{items.map((item, index) => (
<div
key={index}
draggable
onDragStart={handleDragStart(index)}
onDrop={handleDrop(index)}
onDragOver={handleDragOver}
style={{
padding: '10px',
margin: '5px',
backgroundColor: 'white',
border: '1px solid #ddd',
cursor: 'move'
}}
>
{item}
</div>
))}
</div>
);
}
封装艺术与最佳实践
设计原则
- 单一职责:每个Hook专注于解决一个特定问题
- 组合优先:通过组合简单Hook构建复杂功能
- 依赖明确:清晰定义输入输出,避免隐式依赖
- 错误边界:提供适当的错误处理和恢复机制
进阶模式
// Hook工厂模式 - 创建可配置的Hook
export function createFetchHook<T>(baseOptions: RequestInit = {}) {
return function useCustomFetch(url: string, options?: RequestInit) {
const mergedOptions = { ...baseOptions, ...options };
return useFetch<T>(url, mergedOptions);
};
}
// 创建带有认证头的专用Fetch Hook
export const useAuthFetch = createFetchHook({
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
'Content-Type': 'application/json'
}
});
// Hook组合示例
export function useUserDashboard(userId: string) {
const user = useQuery(['user', userId], () => fetchUser(userId));
const posts = useQuery(['posts', userId], () => fetchUserPosts(userId));
const stats = useQuery(['stats', userId], () => fetchUserStats(userId));
const isLoading = user.loading || posts.loading || stats.loading;
const error = user.error || posts.error || stats.error;
return {
user: user.data,
posts: posts.data,
stats: stats.data,
isLoading,
error,
refetch: () => {
user.refetch?.();
posts.refetch?.();
stats.refetch?.();
}
};
}
总结
自定义Hook是现代React开发的"炼金术",它将复杂的业务逻辑转化为可重用、可组合、可测试的代码单元。从简单的数据请求到复杂的拖拽交互,合理的Hook封装能够显著提升代码质量、开发效率和团队协作能力。掌握这门艺术,意味着掌握了构建可维护、可扩展React应用的关键技能。
通过本文的实例可以看到,优秀的自定义Hook设计不仅解决了具体的技术问题,更重要的是建立了一套可扩展的模式和架构思想,这正是前端工程化向更高层次迈进的重要标志。

191

被折叠的 条评论
为什么被折叠?



