自定义 Hook 是怎么从 TodoList 中长出来的?设计思路值得借鉴吗?

从TodoList实战看自定义Hook的进化之路:可复用的设计哲学

在React开发中,我们常常会陷入这样的困境:当多个组件需要共享相同业务逻辑时,要么复制粘贴代码块,要么陷入高阶组件和Render Props的嵌套地狱。本文将以TodoList这一经典案例为切入点,揭示自定义Hook如何从具体业务场景中自然生长,并探讨其背后值得借鉴的架构设计思维。

一、TodoList暴露的传统开发痛点

1.1 标准实现的结构缺陷

在传统的TodoList组件中,我们通常会将状态管理和视图渲染混为一谈:

function TodoList() {
  const [todos, setTodos] = useState([])
  const [input, setInput] = useState('')

  // 业务逻辑与UI代码交织
  const addTodo = () => {...}
  const toggleTodo = () => {...}
  const deleteTodo = () => {...}

  return (/ 大量JSX代码 /)
}

这种实现方式导致三个突出问题:

  • 逻辑复用成本高:其他组件需要使用相同逻辑时必须复制代码
  • 可维护性差:业务逻辑分散在生命周期方法和事件处理器中
  • 测试复杂度高:需要渲染完整组件才能测试核心逻辑

1.2 破局者的诞生契机

当项目需要新增以下功能时,问题集中爆发:

  1. 多个视图需要同步显示待办事项统计
  2. 移动端和PC端需要共享核心逻辑
  3. 添加本地存储持久化功能

二、自定义Hook的破茧之路

2.1 逻辑提取的进化过程

我们通过四步完成逻辑抽象:

阶段 代码形态 复用性
原始阶段 内联在组件中 0%
初级重构 工具函数封装 30%
进阶方案 高阶组件封装 60%
终极形态 自定义Hook 100%

2.2 useTodos Hook的具象实现

// src/hooks/useTodos.js
import { useState, useEffect } from "react";

export default function useTodos(initialValue) {
  const [todos, setTodos] = useState(initialValue);
  
  // 本地存储同步
  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]);

  const addTodo = text => {
    setTodos([...todos, { text, completed: false }]);
  };

  const toggleTodo = index => {
    const newTodos = todos.map((todo, i) => 
      i === index ? {...todo, completed: !todo.completed} : todo
    );
    setTodos(newTodos);
  };

  return { todos, addTodo, toggleTodo };
}

2.3 组件层的精简蜕变

重构后的组件变得异常简洁:

function TodoList() {
  const { todos, addTodo, toggleTodo } = useTodos([]);
  
  return (
    <div>
      {/ 仅保留UI渲染逻辑 /}
    </div>
  )
}

三、值得借鉴的设计哲学

3.1 模块化设计原则

  • 单一职责原则:每个Hook只处理特定领域逻辑
  • 开闭原则:扩展时不修改原有Hook,通过组合实现功能增强
  • 依赖倒置原则:组件依赖抽象接口而非具体实现

3.2 渐进式抽象策略

建议遵循以下重构步骤:

  1. 识别重复代码区块
  2. 创建临时Hook进行逻辑收拢
  3. 通过参数化提高灵活性
  4. 添加类型定义和文档注释

3.3 可测试性设计

通过Hook的独立封装,我们可以直接测试业务逻辑:

test('toggleTodo should invert completion status', () => {
  const { result } = renderHook(() => useTodos([{text: 'test', completed: false}]));
  
  act(() => {
    result.current.toggleTodo(0);
  });
  
  expect(result.current.todos[0].completed).toBe(true);
});

四、模式扩展与最佳实践

4.1 组合式Hook开发

function usePersistedTodos() {
  const todos = useTodos([]);
  useSyncToCloud(todos);
  return todos;
}

4.2 性能优化技巧

  • 使用useCallback缓存事件处理器
  • 通过useMemo避免不必要的计算
  • 拆分高频更新逻辑到独立Hook

这种从具体业务场景中自然生长出的架构方案,完美诠释了"从实践中来,到实践中去"的设计哲学。当我们将TodoList的解决方案上升到模式层面,就能在各类业务场景中游刃有余地构建高可维护的前端架构。