其实写代码和整理房间很像 ------ 东西少的时候随便堆也没关系,但东西多了就得有收纳逻辑。自定义 Hook 就是前端开发里的 "收纳神器",尤其像 TodoList 这种需要重复使用相似功能的场景,更能体现它的设计智慧。
一、先看为什么需要自定义 Hook?
做 TodoList 时,我最开始把所有逻辑都写在组件里:
jsx
// 这是一个混杂了各种逻辑的组件
function TodoList() {
// 1. 管理状态
const [todos, setTodos] = useState([]);
const [inputText, setInputText] = useState('');
// 2. 处理本地存储
useEffect(() => {
const saved = localStorage.getItem('todos');
if (saved) setTodos(JSON.parse(saved));
}, []);
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
// 3. 处理业务逻辑
const addTodo = () => {
if (!inputText.trim()) return;
setTodos([...todos, { id: Date.now(), text: inputText, done: false }]);
setInputText('');
};
const toggleTodo = (id) => {
setTodos(todos.map(t => t.id === id ? { ...t, done: !t.done } : t));
};
// 4. 渲染视图
return (
<div>
<input
value={inputText}
onChange={e => setInputText(e.target.value)}
/>
<button onClick={addTodo}>添加</button>
<ul>
{todos.map(todo => (
<li key={todo.id} onClick={() => toggleTodo(todo.id)}>
{todo.done ? <s>{todo.text}</s> : todo.text}
</li>
))}
</ul>
</div>
);
}
当我需要第二个 "工作清单" 组件时,只能复制粘贴后修改存储键名。这就像把衣服、书籍、杂物都堆在一个箱子里,想找东西得翻半天,想复制一份还得把所有东西都倒出来挑拣。复杂,麻烦
二、自定义 Hook 的核心设计思想
自定义 Hook 的出现,本质上是为了解决 "状态逻辑复用" 的问题。它的设计思想可以浓缩成三个词:抽离共性、隔离状态、按需组合。
1. 抽离共性
TodoList 里有很多可复用的逻辑:状态管理、本地存储、增删改查方法。这些就像家里的 "收纳盒"------ 不管放衣服还是书籍,收纳盒的结构是通用的。
jsx
// 这就是一个最简化的自定义Hook
function useTodo(storageKey = 'todos') {
// 1. 状态管理
const [todos, setTodos] = useState([]);
const [inputText, setInputText] = useState('');
// 2. 副作用处理(本地存储)
useEffect(() => {
const saved = localStorage.getItem(storageKey);
if (saved) setTodos(JSON.parse(saved));
}, [storageKey]);
useEffect(() => {
localStorage.setItem(storageKey, JSON.stringify(todos));
}, [todos, storageKey]);
// 3. 核心方法
const addTodo = () => {
if (!inputText.trim()) return;
setTodos(prev => [...prev, { id: Date.now(), text: inputText, done: false }]);
setInputText('');
};
const toggleTodo = (id) => {
setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t));
};
// 暴露需要的状态和方法
return {
todos,
inputText,
setInputText,
addTodo,
toggleTodo
};
}
这个 Hook 抽离了所有 TodoList 共有的逻辑,但通过storageKey参数保留了灵活性 ------ 就像一个带可替换标签的收纳盒,既能放袜子也能放内裤,只需换个标签。
2. 隔离状态
用 Hook 的时候,最神奇的是状态隔离:
csharp
// 第一个组件
function LifeTodo() {
const { todos, inputText, setInputText, addTodo, toggleTodo } = useTodo('lifeTodos');
// 渲染...
}
// 第二个组件
function WorkTodo() {
const { todos, inputText, setInputText, addTodo, toggleTodo } = useTodo('workTodos');
// 渲染...
}
虽然用的是同一个useTodo,但 LifeTodo 和 WorkTodo 的状态完全独立。这就像两个长得一样的收纳盒,一个放客厅一个放卧室,里面的东西互不干扰。
这种隔离性是通过函数闭包实现的 ------ 每次调用 Hook 都会创建新的状态变量,就像每次新建收纳盒都会分配新的空间。
3. 按需组合
复杂功能可以通过组合多个小 Hook 实现。比如我想给 TodoList 加个统计功能,可以写个useTodoStats:
jsx
// 统计Hook
function useTodoStats(todos) {
const total = todos.length;
const completed = todos.filter(t => t.done).length;
const progress = total > 0 ? Math.round(completed / total * 100) : 0;
return { total, completed, progress };
}
// 在组件中组合使用
function TodoList() {
const { todos, ...rest } = useTodo();
const { total, completed, progress } = useTodoStats(todos);
return (
<div>
{/* 使用useTodo提供的功能 */}
{/* 显示useTodoStats提供的统计 */}
<div>完成度:{progress}%</div>
</div>
);
}
这种组合思想就像乐高积木 ------ 不用每次都造新零件,用现成的小模块就能拼出各种造型。
三、设计自定义 Hook 的三个原则
1. 单一职责:一个 Hook 只做一件事
我曾经写过一个useTodoAndUser的 Hook,既管待办又管用户信息,结果越改越乱。后来拆成useTodo和useUser两个 Hook,维护起来清爽多了。
就像收纳盒不能又放食物又放工具,每个 Hook 应该专注于一类逻辑。
2. 声明式设计:告诉 "做什么" 而非 "怎么做"
好的 Hook 应该像点餐 ------ 你只需要说 "我要一杯咖啡",不用告诉服务员 "先磨豆子再煮水"。
比如useTodo的addTodo方法,调用者只需要知道 "调用它能添加待办",不需要知道内部怎么处理状态和存储。
3. 状态私有:暴露必要的,隐藏无关的
useTodo里的setTodos方法是内部用的,不应该暴露给外部;而addTodo、toggleTodo是外部需要的,必须暴露。
这就像手表 ------ 用户只需要看到时间和调节按钮,不需要看到内部的齿轮和发条。
四、为什么说 Hook 改变了我的编程思维?
没学 Hook 之前,我写代码是 "按页面划分" 的 ------ 这个页面需要什么功能就堆什么逻辑。用了 Hook 之后,变成了 "按功能划分"------ 这个功能可能被多个页面用到,我要把它设计成通用模块。
这种思维转变在做 TodoList 时特别明显:
-
以前加新功能,我会想 "在这个组件里怎么实现"
-
现在加新功能,我会想 "这个功能能不能做成 Hook,以后别的地方也能用"
就像整理房间,以前是 "这个角落放什么",现在是 "这些东西属于哪一类,应该用什么收纳方式"。
最后:Hook 的本质是逻辑的 "模块化封装"
自定义 Hook 其实没什么高深的,它就是把组件里重复的状态逻辑抽出来,装到一个函数里。这个函数能管理自己的状态,能处理副作用,还能把必要的接口暴露给组件使用。
用useTodo重构 TodoList 的过程,就像把散落在房间各个角落的 "待办相关物品"------ 状态管理、存储逻辑、操作方法 ------ 整理到一个带标签的收纳盒里。以后不管在哪个房间(组件)需要用,直接把这个盒子拿过去就行。
这就是自定义 Hook 的核心设计思想:用函数封装状态逻辑,让复用变得简单而优雅。