从 TodoList 看自定义 Hook 的设计思想

其实写代码和整理房间很像 ------ 东西少的时候随便堆也没关系,但东西多了就得有收纳逻辑。自定义 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 的核心设计思想:用函数封装状态逻辑,让复用变得简单而优雅

相关推荐
计算机毕设定制辅导-无忧学长11 分钟前
InfluxDB 集群部署与高可用方案(二)
java·linux·前端
袁煦丞16 分钟前
MongoDB数据存储界的瑞士军刀:cpolar内网穿透实验室第513号成功挑战
前端·程序员·远程工作
天才熊猫君1 小时前
npm 和 pnpm 的一些理解
前端
飞飞飞仔1 小时前
从 Cursor AI 到 Claude Code AI:我的辅助编程转型之路
前端
qb1 小时前
vue3.5.18源码:调试方式
前端·vue.js·架构
Spider_Man2 小时前
缓存策略大乱斗:让你的页面快到飞起!
前端·http·node.js
前端老鹰2 小时前
CSS overscroll-behavior:解决滚动穿透的 “边界控制” 专家
前端·css·html
一叶怎知秋2 小时前
【openlayers框架学习】九:openlayers中的交互类(select和draw)
前端·javascript·笔记·学习·交互
allenlluo2 小时前
浅谈Web Components
前端·javascript
Mintopia2 小时前
把猫咪装进 public/ 文件夹:Next.js 静态资源管理的魔幻漂流
前端·javascript·next.js