自定义Hooks #
一、自定义Hook概述 #
1.1 什么是自定义Hook #
自定义 Hook 是一个以 use 开头的函数,可以调用其他 Hook。
jsx
function useCustomHook() {
const [state, setState] = useState(initialValue);
// ... 其他逻辑
return state;
}
1.2 为什么使用自定义Hook #
| 优势 | 说明 |
|---|---|
| 复用 | 提取可复用逻辑 |
| 简洁 | 简化组件代码 |
| 测试 | 独立测试逻辑 |
| 组合 | 组合多个 Hook |
1.3 命名约定 #
jsx
// 必须以 use 开头
function useWindowSize() { }
function useLocalStorage() { }
function useFetch() { }
function useDebounce() { }
二、基础示例 #
2.1 窗口大小 #
jsx
import { useState, useEffect } from 'preact/hooks';
function useWindowSize() {
const [size, setSize] = useState({
width: typeof window !== 'undefined' ? window.innerWidth : 0,
height: typeof window !== 'undefined' ? window.innerHeight : 0
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
// 使用
function ResponsiveComponent() {
const { width, height } = useWindowSize();
return (
<div>
<p>Width: {width}</p>
<p>Height: {height}</p>
{width < 768 ? <MobileView /> : <DesktopView />}
</div>
);
}
2.2 本地存储 #
jsx
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
if (typeof window === 'undefined') {
return initialValue;
}
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
}, [key, value]);
return [value, setValue];
}
// 使用
function TodoApp() {
const [todos, setTodos] = useLocalStorage('todos', []);
const addTodo = (text) => {
setTodos([...todos, { id: Date.now(), text }]);
};
return (
<div>
<button onClick={() => addTodo('New Task')}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}
2.3 切换状态 #
jsx
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 };
}
// 使用
function Modal() {
const { value: isOpen, toggle, setFalse: close } = useToggle();
return (
<div>
<button onClick={toggle}>Open Modal</button>
{isOpen && (
<div class="modal">
<p>Modal Content</p>
<button onClick={close}>Close</button>
</div>
)}
</div>
);
}
三、数据获取 Hooks #
3.1 useFetch #
jsx
function useFetch(url) {
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);
setError(null);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
if (!cancelled) {
setData(json);
}
} catch (e) {
if (!cancelled) {
setError(e.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchData();
return () => {
cancelled = true;
};
}, [url]);
return { data, loading, error };
}
// 使用
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
3.2 useFetch with Options #
jsx
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
setData(json);
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
}, [url, JSON.stringify(options)]);
useEffect(() => {
if (options.manual) return;
fetchData();
}, [fetchData, options.manual]);
return { data, loading, error, refetch: fetchData };
}
// 使用
function UserList() {
const { data, loading, error, refetch } = useFetch('/api/users', {
manual: false
});
return (
<div>
<button onClick={refetch}>Refresh</button>
{loading && <p>Loading...</p>}
{error && <p>Error: {error}</p>}
<ul>
{data?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
四、定时器 Hooks #
4.1 useInterval #
jsx
function useInterval(callback, delay) {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay === null) return;
const id = setInterval(() => {
savedCallback.current();
}, delay);
return () => clearInterval(id);
}, [delay]);
}
// 使用
function Counter() {
const [count, setCount] = useState(0);
useInterval(() => {
setCount(c => c + 1);
}, 1000);
return <h1>{count}</h1>;
}
// 可暂停的定时器
function PauseableTimer() {
const [count, setCount] = useState(0);
const [isRunning, setIsRunning] = useState(true);
useInterval(
() => setCount(c => c + 1),
isRunning ? 1000 : null
);
return (
<div>
<h1>{count}</h1>
<button onClick={() => setIsRunning(!isRunning)}>
{isRunning ? 'Pause' : 'Resume'}
</button>
</div>
);
}
4.2 useTimeout #
jsx
function useTimeout(callback, delay) {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay === null) return;
const id = setTimeout(() => {
savedCallback.current();
}, delay);
return () => clearTimeout(id);
}, [delay]);
}
// 使用
function Notification({ message, onClose }) {
useTimeout(onClose, 3000);
return <div class="notification">{message}</div>;
}
五、防抖与节流 #
5.1 useDebounce #
jsx
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// 使用
function Search() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
searchAPI(debouncedQuery);
}
}, [debouncedQuery]);
return (
<input
value={query}
onInput={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
);
}
5.2 useDebouncedCallback #
jsx
function useDebouncedCallback(callback, delay, deps = []) {
const timeoutRef = useRef(null);
const debouncedCallback = useCallback((...args) => {
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
callback(...args);
}, delay);
}, deps);
useEffect(() => {
return () => clearTimeout(timeoutRef.current);
}, []);
return debouncedCallback;
}
// 使用
function Search() {
const handleSearch = useDebouncedCallback((query) => {
searchAPI(query);
}, 300);
return (
<input
onInput={(e) => handleSearch(e.target.value)}
placeholder="Search..."
/>
);
}
5.3 useThrottle #
jsx
function useThrottle(value, delay) {
const [throttledValue, setThrottledValue] = useState(value);
const lastExecuted = useRef(Date.now());
useEffect(() => {
const now = Date.now();
const timeSinceLastExecution = now - lastExecuted.current;
if (timeSinceLastExecution >= delay) {
lastExecuted.current = now;
setThrottledValue(value);
} else {
const timer = setTimeout(() => {
lastExecuted.current = Date.now();
setThrottledValue(value);
}, delay - timeSinceLastExecution);
return () => clearTimeout(timer);
}
}, [value, delay]);
return throttledValue;
}
六、表单相关 #
6.1 useFormInput #
jsx
function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);
const handleChange = useCallback((e) => {
setValue(e.target.value);
}, []);
const reset = useCallback(() => {
setValue(initialValue);
}, [initialValue]);
return {
value,
onChange: handleChange,
reset
};
}
// 使用
function LoginForm() {
const username = useFormInput('');
const password = useFormInput('');
const handleSubmit = (e) => {
e.preventDefault();
console.log(username.value, password.value);
};
return (
<form onSubmit={handleSubmit}>
<input {...username} placeholder="Username" />
<input {...password} type="password" placeholder="Password" />
<button type="submit">Login</button>
</form>
);
}
6.2 useForm #
jsx
function useForm(initialValues, validate) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const handleChange = useCallback((e) => {
const { name, value, type, checked } = e.target;
setValues(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
}, []);
const handleBlur = useCallback((e) => {
const { name } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
}, []);
const validateForm = useCallback(() => {
if (validate) {
const validationErrors = validate(values);
setErrors(validationErrors);
return Object.keys(validationErrors).length === 0;
}
return true;
}, [values, validate]);
const reset = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
}, [initialValues]);
useEffect(() => {
validateForm();
}, [values]);
return {
values,
errors,
touched,
handleChange,
handleBlur,
validateForm,
reset,
isValid: Object.keys(errors).length === 0
};
}
// 使用
function RegistrationForm() {
const validate = (values) => {
const errors = {};
if (!values.username) {
errors.username = 'Username is required';
}
if (!values.email) {
errors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(values.email)) {
errors.email = 'Email is invalid';
}
if (!values.password) {
errors.password = 'Password is required';
} else if (values.password.length < 6) {
errors.password = 'Password must be at least 6 characters';
}
return errors;
};
const {
values,
errors,
touched,
handleChange,
handleBlur,
reset,
isValid
} = useForm(
{ username: '', email: '', password: '' },
validate
);
const handleSubmit = (e) => {
e.preventDefault();
if (isValid) {
console.log('Submit:', values);
reset();
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
name="username"
value={values.username}
onInput={handleChange}
onBlur={handleBlur}
placeholder="Username"
/>
{touched.username && errors.username && (
<span class="error">{errors.username}</span>
)}
</div>
<div>
<input
name="email"
value={values.email}
onInput={handleChange}
onBlur={handleBlur}
placeholder="Email"
/>
{touched.email && errors.email && (
<span class="error">{errors.email}</span>
)}
</div>
<div>
<input
name="password"
type="password"
value={values.password}
onInput={handleChange}
onBlur={handleBlur}
placeholder="Password"
/>
{touched.password && errors.password && (
<span class="error">{errors.password}</span>
)}
</div>
<button type="submit" disabled={!isValid}>
Register
</button>
</form>
);
}
七、其他实用 Hooks #
7.1 usePrevious #
jsx
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>Current: {count}</p>
<p>Previous: {prevCount}</p>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}
7.2 useClickOutside #
jsx
function useClickOutside(ref, callback) {
useEffect(() => {
const handleClick = (e) => {
if (ref.current && !ref.current.contains(e.target)) {
callback();
}
};
document.addEventListener('mousedown', handleClick);
document.addEventListener('touchstart', handleClick);
return () => {
document.removeEventListener('mousedown', handleClick);
document.removeEventListener('touchstart', handleClick);
};
}, [ref, callback]);
}
// 使用
function Dropdown() {
const [isOpen, setIsOpen] = useState(false);
const ref = useRef(null);
useClickOutside(ref, () => setIsOpen(false));
return (
<div ref={ref}>
<button onClick={() => setIsOpen(!isOpen)}>
Toggle
</button>
{isOpen && (
<ul class="dropdown-menu">
<li>Option 1</li>
<li>Option 2</li>
</ul>
)}
</div>
);
}
7.3 useMediaQuery #
jsx
function useMediaQuery(query) {
const [matches, setMatches] = useState(() => {
if (typeof window !== 'undefined') {
return window.matchMedia(query).matches;
}
return false;
});
useEffect(() => {
const mediaQuery = window.matchMedia(query);
const handleChange = (e) => {
setMatches(e.matches);
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, [query]);
return matches;
}
// 使用
function ResponsiveComponent() {
const isMobile = useMediaQuery('(max-width: 768px)');
const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
const isDesktop = useMediaQuery('(min-width: 1025px)');
return (
<div>
{isMobile && <MobileView />}
{isTablet && <TabletView />}
{isDesktop && <DesktopView />}
</div>
);
}
八、最佳实践 #
8.1 单一职责 #
jsx
// 好的做法:每个 Hook 只做一件事
function useWindowSize() { }
function useScrollPosition() { }
// 避免:一个 Hook 做太多事
function useWindowInfo() {
// 同时处理 size 和 scroll
}
8.2 返回值设计 #
jsx
// 单个值
function useToggle() {
return [value, toggle];
}
// 多个值:返回对象
function useForm() {
return { values, errors, handleChange, handleSubmit };
}
8.3 错误处理 #
jsx
function useCustomHook() {
const context = useContext(SomeContext);
if (!context) {
throw new Error('useCustomHook must be used within Provider');
}
return context;
}
九、总结 #
| 要点 | 说明 |
|---|---|
| 命名 | 以 use 开头 |
| 复用 | 提取可复用逻辑 |
| 组合 | 组合其他 Hook |
| 测试 | 独立测试逻辑 |
核心原则:
- 遵循 Hook 规则
- 保持单一职责
- 提供清晰的 API
- 处理边界情况
最后更新:2026-03-28