React Hook 自定义实战
React Hooks 让我们可以提取和复用组件逻辑。本文将详细介绍如何创建和使用自定义 Hooks,从基础概念到高级实战,帮助你打造可复用的组件逻辑。
一、Hook 基础概念
1.1 什么是 Hook?
Hook 是 React 16.8 引入的新特性,允许你在不编写 class 的情况下使用 state 等特性。
1.2 内置 Hook
React 提供了以下内置 Hook:
- useState:状态管理
- useEffect:副作用处理
- useContext:上下文访问
- useReducer:复杂状态管理
- useCallback:函数缓存
- useMemo:值缓存
- useRef:引用访问
- useLayoutEffect:布局副作用
- useImperativeHandle:暴露组件实例
- useId:生成唯一 ID
- useSyncExternalStore:外部状态同步
1.3 自定义 Hook
自定义 Hook 是一个函数,其命名以 use 开头,可以调用其他 Hook:
function useLocalStorage(key, initialValue) { const [storedValue, setStoredValue] = useState(() => { const item = localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; });
useEffect(() => { localStorage.setItem(key, JSON.stringify(storedValue)); }, [key, storedValue]);
return [storedValue, setStoredValue]; }
|
二、基础自定义 Hook
2.1 useLocalStorage
管理 localStorage 状态:
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) { const [storedValue, setStoredValue] = useState(() => { if (typeof window === 'undefined') { return initialValue; }
try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { console.error(error); return initialValue; } });
const setValue = (value) => { try { setStoredValue(value); if (typeof window !== 'undefined') { window.localStorage.setItem(key, JSON.stringify(value)); } } catch (error) { console.error(error); } };
return [storedValue, setValue]; }
function App() { const [count, setCount] = useLocalStorage('count', 0);
return ( <div> <p>计数: {count}</p> <button onClick={() => setCount(c => c + 1)}>增加</button> </div> ); }
|
2.2 useMousePosition
监听鼠标位置:
import { useState, useEffect } from 'react';
function useMousePosition() { const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
useEffect(() => { const updatePosition = (event) => { setMousePosition({ x: event.clientX, y: event.clientY }); };
window.addEventListener('mousemove', updatePosition);
return () => { window.removeEventListener('mousemove', updatePosition); }; }, []);
return mousePosition; }
function App() { const { x, y } = useMousePosition();
return ( <div> <p>X: {x}, Y: {y}</p> </div> ); }
|
2.3 useClickOutside
点击外部关闭元素:
import { useState, useEffect, useRef } from 'react';
function useClickOutside(ref, handler) { useEffect(() => { const listener = (event) => { if (!ref.current || ref.current.contains(event.target)) { return; }
handler(event); };
document.addEventListener('mousedown', listener); document.addEventListener('touchstart', listener);
return () => { document.removeEventListener('mousedown', listener); document.removeEventListener('touchstart', listener); }; }, [ref, handler]); }
function Dropdown() { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null);
useClickOutside(dropdownRef, () => setIsOpen(false));
return ( <div ref={dropdownRef}> <button onClick={() => setIsOpen(!isOpen)}> 下拉菜单 {isOpen ? '▼' : '▲'} </button> {isOpen && ( <ul> <li>选项 1</li> <li>选项 2</li> <li>选项 3</li> </ul> )} </div> ); }
|
2.4 useDebounce
防抖函数:
import { useState, useEffect, useCallback } 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 SearchBox() { const [query, setQuery] = useState(''); const debouncedQuery = useDebounce(query, 500);
useEffect(() => { if (debouncedQuery) { console.log('搜索:', debouncedQuery); } }, [debouncedQuery]);
return ( <input type="text" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="输入搜索..." /> ); }
|
2.5 useThrottle
节流函数:
import { useState, useEffect } from 'react';
function useThrottle(value, limit) { const [throttledValue, setThrottledValue] = useState(value);
useEffect(() => { let timeoutId = null;
const throttledFn = () => { timeoutId = setTimeout(() => { setThrottledValue(value); }, limit); };
throttledFn();
return () => { if (timeoutId) { clearTimeout(timeoutId); } }; }, [value, limit]);
return throttledValue; }
function Counter() { const [count, setCount] = useState(0); const throttledCount = useThrottle(count, 1000);
useEffect(() => { console.log('节流后的计数:', throttledCount); }, [throttledCount]);
return ( <div> <p>计数: {count}</p> <button onClick={() => setCount(c => c + 1)}>增加</button> </div> ); }
|
三、数据获取 Hooks
3.1 useFetch
封装 fetch 请求:
import { useState, useEffect } from 'react';
function useFetch(url, options = {}) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { let cancelled = false;
async function fetchData() { try { setLoading(true); const response = await fetch(url, options); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); }
const result = await response.json(); if (!cancelled) { setData(result); } } catch (err) { if (!cancelled) { setError(err); } } finally { if (!cancelled) { setLoading(false); } } }
fetchData();
return () => { cancelled = true; }; }, [url, options]);
return { data, loading, error }; }
function UserList() { const { data: users, loading, error } = useFetch('https://api.example.com/users');
if (loading) return <p>加载中...</p>; if (error) return <p>错误: {error.message}</p>;
return ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); }
|
3.2 useIntersectionObserver
无限滚动:
import { useState, useEffect, useRef } from 'react';
function useIntersectionObserver(ref, options = {}) { const [isIntersecting, setIntersecting] = useState(false);
useEffect(() => { if (!ref.current) return;
const observer = new IntersectionObserver(([entry]) => { setIntersecting(entry.isIntersecting); }, options);
observer.observe(ref.current);
return () => { if (ref.current) { observer.unobserve(ref.current); } }; }, [ref, options]);
return isIntersecting; }
function InfiniteList({ fetchItems, items }) { const triggerRef = useRef(null); const isIntersecting = useIntersectionObserver(triggerRef);
useEffect(() => { if (isIntersecting && items.length > 0) { fetchMoreItems(); } }, [isIntersecting]);
const fetchMoreItems = async () => { const newItems = await fetchItems(); setItems(prev => [...prev, ...newItems]); };
return ( <div> {items.map(item => <Item key={item.id} item={item} />)} <div ref={triggerRef} style={{ height: '10px' }} /> </div> ); }
|
四、表单 Hook
表单验证和提交:
import { useState, useEffect } from 'react';
function useForm(initialValues, validate, onSubmit) { const [values, setValues] = useState(initialValues); const [errors, setErrors] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => { if (isSubmitting) { const hasErrors = Object.keys(errors).length > 0; if (!hasErrors) { onSubmit(values); } setIsSubmitting(false); } }, [isSubmitting, errors, onSubmit, values]);
const handleChange = (e) => { const { name, value } = e.target; setValues(prev => ({ ...prev, [name]: value }));
if (errors[name]) { setErrors(prev => ({ ...prev, [name]: null })); } };
const handleSubmit = (e) => { e.preventDefault(); const validationErrors = validate(values); setErrors(validationErrors); setIsSubmitting(Object.keys(validationErrors).length === 0); };
const resetForm = () => { setValues(initialValues); setErrors({}); };
return { values, errors, isSubmitting, handleChange, handleSubmit, resetForm }; }
function LoginForm() { const validate = (values) => { const errors = {};
if (!values.username) { errors.username = '用户名不能为空'; }
if (!values.email) { errors.email = '邮箱不能为空'; } else if (!/\S+@\S+\.\S+$/.test(values.email)) { errors.email = '邮箱格式不正确'; }
return errors; };
const handleSubmit = (values) => { console.log('提交表单:', values); };
const { values, errors, isSubmitting, handleChange, handleSubmit: submitForm } = useForm( { username: '', email: '' }, validate, handleSubmit );
return ( <form onSubmit={submitForm}> <div> <label>用户名</label> <input type="text" name="username" value={values.username} onChange={handleChange} className={errors.username ? 'error' : ''} /> {errors.username && <span>{errors.username}</span>} </div>
<div> <label>邮箱</label> <input type="email" name="email" value={values.email} onChange={handleChange} className={errors.email ? 'error' : ''} /> {errors.email && <span>{errors.email}</span>} </div>
<button type="submit" disabled={isSubmitting}> {isSubmitting ? '提交中...' : '提交'} </button> </form> ); }
|
五、主题 Hook
5.1 useTheme
主题切换:
import { useState, useEffect } from 'react';
function useTheme() { const [theme, setTheme] = useState(() => { return localStorage.getItem('theme') || 'light'; });
useEffect(() => { document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('theme', theme); }, [theme]);
const toggleTheme = () => { setTheme(prev => prev === 'light' ? 'dark' : 'light'); };
return { theme, toggleTheme }; }
function App() { const { theme, toggleTheme } = useTheme();
return ( <div> <button onClick={toggleTheme}> 切换到 {theme === 'light' ? '深色' : '浅色'} 主题 </button> </div> ); }
|
六、动画 Hook
6.1 useAnimation
动画控制:
import { useState, useEffect } from 'react';
function useAnimation(initialState) { const [isAnimating, setIsAnimating] = useState(false);
const startAnimation = () => { setIsAnimating(true); };
const stopAnimation = () => { setIsAnimating(false); };
const toggleAnimation = () => { setIsAnimating(prev => !prev); };
return { isAnimating, startAnimation, stopAnimation, toggleAnimation }; }
function AnimatedButton() { const { isAnimating, startAnimation, stopAnimation } = useAnimation(false);
const handleClick = () => { startAnimation(); setTimeout(() => { stopAnimation(); console.log('动画完成'); }, 1000); };
return ( <button onClick={handleClick} className={isAnimating ? 'animating' : ''}> 点击开始动画 </button> ); }
|
七、性能优化 Hooks
7.1 usePrevious
获取上一次的值:
import { useRef, useEffect } from 'react';
function usePrevious(value) { const ref = useRef();
useEffect(() => { ref.current = value; }, [value]);
return ref.current; }
function Counter() { const [count, setCount] = useState(0); const previousCount = usePrevious(count);
useEffect(() => { if (previousCount !== null && count !== previousCount) { console.log(`从 ${previousCount} 变为 ${count}`); } }, [count, previousCount]);
return ( <div> <p>当前: {count}</p> <p>上一次: {previousCount ?? '无'}</p> <button onClick={() => setCount(c => c + 1)}>增加</button> </div> ); }
|
7.2 useLocalStorageReducer
使用 Reducer 管理 localStorage:
import { useReducer, useEffect } from 'react';
function useLocalStorageReducer(key, reducer, initialState) { const [state, dispatch] = useReducer(reducer, initialState, () => { try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : initialState; } catch (error) { return initialState; } });
useEffect(() => { localStorage.setItem(key, JSON.stringify(state)); }, [key, state]);
return [state, dispatch]; }
function Counter() { const [count, dispatch] = useLocalStorageReducer( 'counter', (state, action) => { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; case 'reset': return { count: 0 }; default: return state; } }, { count: 0 } );
return ( <div> <p>计数: {count.count}</p> <button onClick={() => dispatch({ type: 'increment' })}>增加</button> <button onClick={() => dispatch({ type: 'decrement' })}>减少</button> <button onClick={() => dispatch({ type: 'reset' })}>重置</button> </div> ); }
|
八、最佳实践
8.1 命名规范
function useFetchUserData() { } function useLocalStorageTheme() { } function useClickOutsideElement() { }
function f() { } function u() { } function c() { }
|
8.2 错误处理
function useErrorHandling(fetchFn) { const [error, setError] = useState(null);
const execute = async (...args) => { try { setError(null); const result = await fetchFn(...args); return result; } catch (err) { setError(err); throw err; } };
return { execute, error }; }
|
8.3 依赖数组
useEffect(() => { }, [dependency1, dependency2]);
|
8.4 文档注释
function useDebounce(value, delay) { }
|
九、总结
自定义 Hook 的优势:
- 逻辑复用:提取可复用的逻辑
- 代码组织:使组件更清晰
- 可维护性:更容易维护和理解
- 可测试性:可以单独测试 Hook
常见的自定义 Hook:
- 数据获取:useFetch, useInfiniteScroll
- 表单处理:useForm, useInput
- 状态管理:useLocalStorage, useTheme
- 动画:useAnimation, useTransition
- 性能优化:useMemo, useCallback
创建优秀的自定义 Hook 可以让你的代码更简洁、更可维护!