从 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 的核心设计思想:用函数封装状态逻辑,让复用变得简单而优雅

相关推荐
一斤代码1 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子1 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年1 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子2 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina2 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路3 小时前
React--Fiber 架构
前端·react.js·架构
伍哥的传说3 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js
qq_424409193 小时前
uniapp的app项目,某个页面长时间无操作,返回首页
前端·vue.js·uni-app
我在北京coding3 小时前
element el-table渲染二维对象数组
前端·javascript·vue.js
布兰妮甜3 小时前
Vue+ElementUI聊天室开发指南
前端·javascript·vue.js·elementui