太棒了!👏 你已经掌握了 React 的核心机制,现在是时候“造轮子”了!
—— 封装可复用逻辑的艺术
💡 自定义 Hook 是 React 中最强大的抽象机制之一。
它让你把组件逻辑提取成可复用的函数,告别重复代码。
一、什么是自定义 Hook?🧩
✅ 定义:
一个以 use 开头的 JavaScript 函数,内部可以调用其他 Hook(如 useState, useEffect)。
🎯 目标:封装“状态逻辑”而非 UI
| 对比 | 普通函数 | 自定义 Hook |
|---|---|---|
| 能否使用 Hook? | ❌ 不能 | ✅ 可以 |
| 是否有状态? | ❌ 无 | ✅ 有 |
| 用途 | 工具函数(计算、格式化) | 状态 + 副作用逻辑封装 |
二、为什么需要自定义 Hook?🎯
想象你写了 5 个组件,每个都要:
- 从 localStorage 读数据
- 监听输入变化
- 写回 localStorage
👉 你会复制粘贴 5 次吗?❌
✅ 应该封装成 useLocalStorage
✨ 自定义 Hook 解决三大问题:
- 逻辑复用(不是 UI 复用)
- 避免重复代码
- 提升组件可读性
三、实战案例:从零实现几个经典自定义 Hook 💡
🪝 示例 1:useLocalStorage —— 带持久化的 state
// hooks/useLocalStorage.js
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// 从 localStorage 读取初始值
const [value, setValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.warn(`读取 ${key} 失败`, error);
return initialValue;
}
});
// 当 value 变化时,同步到 localStorage
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(`保存 ${key} 失败`, error);
}
}, [key, value]);
return [value, setValue];
}
✅ 使用方式:
function DarkModeToggle() {
const [isDark, setIsDark] = useLocalStorage('theme', false);
return (
<button onClick={() => setIsDark(!isDark)}>
当前主题:{isDark ? '暗黑' : '明亮'}
</button>
);
}
✅ 刷新页面后状态依然保留!
🪝 示例 2:useFetch —— 简化数据请求
// hooks/useFetch.js
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!url) return;
fetch(url)
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(data => {
setData(data);
setError(null);
})
.catch(err => {
setError(err.message);
setData(null);
})
.finally(() => {
setLoading(false);
});
}, [url]); // URL 变化时重新请求
return { data, loading, error };
}
✅ 使用方式:
function UserList() {
const { data: users, loading, error } = useFetch('/api/users');
if (loading) return <p>加载中...</p>;
if (error) return <p>错误:{error}</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
✨ 从此不用在每个组件里重复写 loading/error/data 三件套!
🪝 示例 3:useWindowSize —— 实时获取窗口大小
// hooks/useWindowSize.js
import { useState, useEffect } from 'react';
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; // { width, height }
}
✅ 使用方式:
function ResponsiveDisplay() {
const { width } = useWindowSize();
return (
<div>
当前屏幕宽度:{width}px
{width < 768 ? <p>移动端</p> : <p>桌面端</p>}
</div>
);
}
四、命名规范与最佳实践 ✅
✅ 命名规则
- 必须以
use开头(如useMousePosition,useForm) - 文件名建议统一放在
/hooks目录下
src/
├── components/
├── hooks/
│ ├── useLocalStorage.js
│ ├── useFetch.js
│ └── useWindowSize.js
└── App.js
✅ 设计原则
| 原则 | 说明 |
|---|---|
| 只做一件事 | 一个 Hook 专注一个功能(单一职责) |
| 返回清晰结构 | 返回对象或数组,便于解构使用 |
| 处理边界情况 | 如空 URL、网络错误、SSR 环境(window 不存在) |
| 支持默认值/配置参数 | 让使用者更灵活 |
✅ 改进版 useFetch(支持配置)
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!url) return;
fetch(url, options) // 支持传入 method, headers 等
.then(...) // 同上
}, [url, options.method, options.headers]); // 注意依赖项
return { data, loading, error, refetch: () => {/* 可加重新请求功能 */} };
}
五、常见误区与避坑指南 ⚠️
❌ 错误 1:在条件中调用自定义 Hook
// ❌ 错误
if (isLoggedIn) {
useFetch('/api/profile'); // 不能在条件中调用 Hook
}
✅ 正确做法:把逻辑放进 Hook 内部判断
function useProfileData(isLoggedIn) {
const [data, setData] = useState(null);
useEffect(() => {
if (!isLoggedIn) return;
fetch('/api/profile').then(...);
}, [isLoggedIn]);
return data;
}
❌ 错误 2:返回 JSX 或操作 DOM
// ❌ 错误:这不是组件!
function useAlertMessage() {
return <div className="alert">警告!</div>; // ❌
}
✅ 正确:返回数据和函数,由组件决定如何渲染
function useAlert() {
const [message, setMessage] = useState('');
const showAlert = (msg) => setMessage(msg);
const hideAlert = () => setMessage('');
return { message, showAlert, hideAlert };
}
六、实战练习 🏋️♂️
✅ 练习 1:useInput —— 表单输入管理
目标:封装受控输入的通用逻辑
function useInput(initialValue) {
const [value, setValue] = useState(initialValue);
const bind = {
value,
onChange: (e) => setValue(e.target.value),
};
return [value, bind, setValue];
}
// 使用
const [username, usernameBind] = useInput('');
<input placeholder="用户名" {...usernameBind} />
✅ 练习 2:useOnlineStatus —— 检测网络状态
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const goOnline = () => setIsOnline(true);
const goOffline = () => setIsOnline(false);
window.addEventListener('online', goOnline);
window.addEventListener('offline', goOffline);
return () => {
window.removeEventListener('online', goOnline);
window.removeEventListener('offline', goOffline);
};
}, []);
return isOnline;
}
// 使用
function StatusIndicator() {
const isOnline = useOnlineStatus();
return <div>网络状态:{isOnline ? '在线' : '离线'}</div>;
}
✅ 总结:自定义 Hook 核心要点
| 要点 | 说明 |
|---|---|
以 use 开头 | 命名约定,让 React 识别 |
| 可调用其他 Hook | 这是它与普通函数的本质区别 |
| 封装逻辑而非 UI | 返回状态、函数、数据 |
| 支持参数和返回值 | 灵活配置,易于使用 |
| 避免条件调用 | 必须在顶层调用 |
| 可组合使用 | 如 useFetch + useLocalStorage |
🎯 下一步预告:
你已经能“造轮子”了!接下来我们要进入工程化阶段:
➡️ React 状态管理进阶:Context + useReducer,迈向 Redux
是否继续?我将带你构建一个真正的全局状态管理系统。