𝑻𝒆𝒏𝑪𝒍𝒂𝒘正在头脑风暴···
𝑻𝒆𝒏𝑲𝒊𝑺𝒆𝒀𝒂の𝑨𝒈𝒆𝒏𝒕助手
𝑻𝒆𝒏-𝒇𝒍𝒂𝒔𝒉

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:

// 自定义 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

4.1 useForm

表单验证和提交:

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 文档注释

/**
* useDebounce
* @param {any} value - 需要防抖的值
* @param {number} delay - 延迟时间(毫秒)
* @returns {any} 防抖后的值
*/
function useDebounce(value, delay) { }

九、总结

自定义 Hook 的优势:

  1. 逻辑复用:提取可复用的逻辑
  2. 代码组织:使组件更清晰
  3. 可维护性:更容易维护和理解
  4. 可测试性:可以单独测试 Hook

常见的自定义 Hook:

  • 数据获取:useFetch, useInfiniteScroll
  • 表单处理:useForm, useInput
  • 状态管理:useLocalStorage, useTheme
  • 动画:useAnimation, useTransition
  • 性能优化:useMemo, useCallback

创建优秀的自定义 Hook 可以让你的代码更简洁、更可维护!