自定义Hooks #
一、自定义Hooks概述 #
1.1 什么是自定义Hook #
自定义 Hook 是一个以 use 开头的函数,可以调用其他 Hook。
javascript
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
1.2 为什么使用自定义Hook #
| 优势 | 说明 |
|---|---|
| 逻辑复用 | 在多个组件中复用相同逻辑 |
| 代码组织 | 将复杂逻辑抽离成独立模块 |
| 关注分离 | 组件关注UI,Hook关注逻辑 |
| 可测试 | 独立的逻辑更易于测试 |
1.3 命名约定 #
javascript
// ✅ 正确:以use开头
function useWindowSize() {}
function useFetch() {}
function useLocalStorage() {}
// ❌ 错误:不以use开头
function getWindowSize() {}
function fetch() {}
二、常用自定义Hooks #
2.1 useLocalStorage #
javascript
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setStoredValue = useCallback((value) => {
try {
const valueToStore = value instanceof Function ? value(value) : value;
setValue(valueToStore);
localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
}, [key]);
return [value, setStoredValue];
}
// 使用
function App() {
const [name, setName] = useLocalStorage('name', '');
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="输入名字"
/>
);
}
2.2 useFetch #
javascript
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
const controller = new AbortController();
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
if (isMounted) {
setData(json);
setError(null);
}
} catch (err) {
if (isMounted && err.name !== 'AbortError') {
setError(err.message);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
fetchData();
return () => {
isMounted = false;
controller.abort();
};
}, [url]);
return { data, loading, error };
}
// 使用
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <div>加载中...</div>;
if (error) return <div>错误: {error}</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
2.3 useToggle #
javascript
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 (
<>
<button onClick={toggle}>打开弹窗</button>
{isOpen && (
<div className="modal">
<p>弹窗内容</p>
<button onClick={close}>关闭</button>
</div>
)}
</>
);
}
2.4 useDebounce #
javascript
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) {
search(debouncedQuery);
}
}, [debouncedQuery]);
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索..."
/>
);
}
2.5 useWindowSize #
javascript
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
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 } = useWindowSize();
return (
<div>
{width < 768 ? <MobileLayout /> : <DesktopLayout />}
</div>
);
}
2.6 usePrevious #
javascript
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>当前: {count}</p>
<p>之前: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
);
}
2.7 useClickOutside #
javascript
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 ref = useRef();
useClickOutside(ref, () => setIsOpen(false));
return (
<div ref={ref}>
<button onClick={() => setIsOpen(!isOpen)}>切换</button>
{isOpen && <div className="dropdown">下拉内容</div>}
</div>
);
}
2.8 useKeyPress #
javascript
function useKeyPress(targetKey) {
const [pressed, setPressed] = useState(false);
useEffect(() => {
const downHandler = ({ key }) => {
if (key === targetKey) {
setPressed(true);
}
};
const upHandler = ({ key }) => {
if (key === targetKey) {
setPressed(false);
}
};
window.addEventListener('keydown', downHandler);
window.addEventListener('keyup', upHandler);
return () => {
window.removeEventListener('keydown', downHandler);
window.removeEventListener('keyup', upHandler);
};
}, [targetKey]);
return pressed;
}
// 使用
function Game() {
const upPressed = useKeyPress('ArrowUp');
const downPressed = useKeyPress('ArrowDown');
return (
<div>
<p>上: {upPressed ? '按下' : '未按下'}</p>
<p>下: {downPressed ? '按下' : '未按下'}</p>
</div>
);
}
三、高级自定义Hooks #
3.1 useAsync #
javascript
function useAsync(asyncFunction, immediate = true) {
const [state, setState] = useState({
loading: immediate,
data: null,
error: null
});
const execute = useCallback(async (...params) => {
setState({ loading: true, data: null, error: null });
try {
const data = await asyncFunction(...params);
setState({ loading: false, data, error: null });
return data;
} catch (error) {
setState({ loading: false, data: null, error });
throw error;
}
}, [asyncFunction]);
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return { ...state, execute };
}
// 使用
function UserList() {
const { data: users, loading, error, execute } = useAsync(
() => fetch('/api/users').then(res => res.json())
);
if (loading) return <div>加载中...</div>;
if (error) return <div>错误: {error.message}</div>;
return (
<div>
<button onClick={execute}>刷新</button>
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
3.2 useForm #
javascript
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 }));
if (validate) {
const validationErrors = validate(values);
setErrors(validationErrors);
}
}, [values, validate]);
const handleSubmit = useCallback((onSubmit) => (e) => {
e.preventDefault();
if (validate) {
const validationErrors = validate(values);
setErrors(validationErrors);
setTouched(
Object.keys(values).reduce((acc, key) => ({ ...acc, [key]: true }), {})
);
if (Object.keys(validationErrors).length > 0) {
return;
}
}
onSubmit(values);
}, [values, validate]);
const reset = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
}, [initialValues]);
return {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
reset
};
}
// 使用
function LoginForm() {
const {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
reset
} = useForm(
{ email: '', password: '' },
(values) => {
const errors = {};
if (!values.email) errors.email = '邮箱必填';
if (!values.password) errors.password = '密码必填';
return errors;
}
);
const onSubmit = (data) => {
console.log('提交:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
name="email"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
/>
{touched.email && errors.email && <span>{errors.email}</span>}
</div>
<div>
<input
name="password"
type="password"
value={values.password}
onChange={handleChange}
onBlur={handleBlur}
/>
{touched.password && errors.password && <span>{errors.password}</span>}
</div>
<button type="submit">登录</button>
<button type="button" onClick={reset}>重置</button>
</form>
);
}
3.3 useIntersectionObserver #
javascript
function useIntersectionObserver(options = {}) {
const [entry, setEntry] = useState(null);
const [node, setNode] = useState(null);
const observer = useRef(null);
useEffect(() => {
if (observer.current) {
observer.current.disconnect();
}
observer.current = new IntersectionObserver(([entry]) => {
setEntry(entry);
}, options);
if (node) {
observer.current.observe(node);
}
return () => {
if (observer.current) {
observer.current.disconnect();
}
};
}, [node, options.threshold, options.root, options.rootMargin]);
return [setNode, entry];
}
// 使用
function LazyImage({ src, alt }) {
const [setRef, entry] = useIntersectionObserver({
threshold: 0.1
});
const [loaded, setLoaded] = useState(false);
useEffect(() => {
if (entry?.isIntersecting && !loaded) {
setLoaded(true);
}
}, [entry?.isIntersecting, loaded]);
return (
<div ref={setRef}>
{loaded ? (
<img src={src} alt={alt} />
) : (
<div className="placeholder">加载中...</div>
)}
</div>
);
}
四、最佳实践 #
4.1 单一职责 #
javascript
// ✅ 好的实践:每个Hook只做一件事
function useUserName(userId) {
const { data } = useFetch(`/api/users/${userId}`);
return data?.name;
}
function useUserEmail(userId) {
const { data } = useFetch(`/api/users/${userId}`);
return data?.email;
}
// ❌ 不好的实践:一个Hook做太多事
function useUserData(userId) {
// 获取用户数据、处理权限、发送统计...
}
4.2 返回值设计 #
javascript
// 数组返回值(适合简单场景)
const [value, toggle] = useToggle();
// 对象返回值(适合复杂场景)
const { data, loading, error, refetch } = useFetch(url);
// 返回多个值
const { value, setValue, reset, hasChanges } = useFormState(initialValue);
4.3 参数设计 #
javascript
// 使用对象参数,便于扩展
function useFetch(url, options = {}) {
const {
method = 'GET',
headers = {},
body = null,
immediate = true
} = options;
// ...
}
// 使用
useFetch('/api/users', {
method: 'POST',
body: { name: 'Alice' }
});
五、总结 #
| 要点 | 说明 |
|---|---|
| 命名 | 以use开头 |
| 单一职责 | 每个Hook只做一件事 |
| 返回值 | 根据复杂度选择数组或对象 |
| 参数 | 使用对象参数便于扩展 |
核心原则:
- 自定义 Hook 用于复用逻辑
- 遵循 Hook 规则
- 保持 Hook 简单和专注
- 提供清晰的 API
最后更新:2026-03-26