React Hooks 实战:从一个 Todo List 看自定义 Hooks 的优雅与坑

51 阅读4分钟

React Hooks 实战:从一个 Todo List 看自定义 Hooks 的优雅与坑

大家好,自从 React 16.8 引入 Hooks 以来,它彻底改变了我们写组件的方式。过去类组件的生命周期散乱、状态逻辑复用难的问题,现在用 Hooks 可以轻松解决。Hooks 不只是“函数式状态管理”,它是一种函数式编程思想,让代码更模块化、更易维护、更可复用。

一、为什么选择 Hooks?类组件的痛点与 Hooks 的救赎

先回忆一下类组件的痛点:

  • 生命周期分散:同一个功能(如数据获取)可能散落在 componentDidMount、componentDidUpdate 中,代码阅读性差。
  • 状态逻辑复用难:HOC(高阶组件)或 Render Props 会导致“包裹地狱”,组件树层级爆炸。
  • this 绑定麻烦:箭头函数、bind() 到处飞,容易出错。

Hooks 的出现,像一股清风:

  • 函数式组件更纯粹:UI 就是 HTML + CSS,业务逻辑抽到 Hooks 中。
  • 逻辑复用无痛:自定义 Hooks 让状态逻辑像普通函数一样复用,不增加组件层级。
  • 代码更聚合:相关逻辑集中在一个 Hook 中,可读性爆棚。

在我们的 Todo List 项目中,就完美体现了这一点:todos 的增删改查全封装在 useTodos Hook 中,鼠标位置实时跟踪封装在 useMouse 中。组件只负责渲染,干净利落!

二、核心案例一:useTodos —— 持久化 Todo 列表的完美封装

来看一个自定义 Hook:useTodos.js

import { useState, useEffect } from "react";

const STORAGE_KEY = 'todos';

function loadFromStorage() {
  const storedTodos = localStorage.getItem(STORAGE_KEY);
  return storedTodos ? JSON.parse(storedTodos) : [];
}

function saveToStorage(todos) {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
}

export const useTodos = () => {
  const [todos, setTodos] = useState(loadFromStorage());

  useEffect(() => {
    saveToStorage(todos);
  }, [todos]);

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

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

  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return { todos, addTodo, toggleTodo, deleteTodo };
};

底层逻辑拆解

  1. 初始状态从 localStorage 加载

    • useState(loadFromStorage()):这里传函数是懒加载,只有第一次渲染时执行。完美避免每次渲染都读 storage 的性能浪费。
  2. 状态变化时持久化

    • useEffect(() => saveToStorage(todos), [todos]):依赖 todos,当 todos 变化时自动保存。经典的“副作用”处理。
  3. 操作函数返回

    • addTodo、toggleTodo、deleteTodo 都是纯函数式更新状态的方式,使用不可变数据(...spread、map、filter),符合 React 最佳实践。

使用方式超级简洁

在 App.jsx 中:

const { todos, addTodo, deleteTodo, toggleTodo } = useTodos();

然后传给子组件:

<TodoInput addTodo={addTodo} />
<TodoList todos={todos} onDelete={deleteTodo} onToggle={toggleTodo} />

组件完全解耦!如果以后想换成 Redux 或 Context,只改 Hook 内部就行。

三、核心案例二:useMouse —— 事件监听与内存泄漏的生死较量

另一个经典 Hook:useMouse.js

export const useMouse = () => {
  const [x, setX] = useState(0);
  const [y, setY] = useState(0);

  useEffect(() => {
    const update = (event) => {
      setX(event.pageX);
      setY(event.pageY);
    };

    window.addEventListener('mousemove', update);

    return () => {
      window.removeEventListener('mousemove', update);
    };
  }, []);  // 空依赖,只挂载一次

  return { x, y };
};

底层逻辑:useEffect 的清理函数

  • useEffect 的返回值是一个清理函数,在组件卸载或下次 effect 执行前调用。
  • 这里监听全局 mousemove,不清理会导致:组件卸载后事件仍绑定,状态更新试图 setState 到已卸载组件 → 内存泄漏警告。

易错点大提醒

  • 忘记 return 清理函数 → 内存泄漏!
  • 依赖数组错写 → 重复绑定或不更新。

类似场景:setInterval、WebSocket、订阅事件,都必须清理。

“组件卸载时,需要清除事件监听/定时器,否则会导致内存泄漏”

四、组件层的设计:纯 UI + Props 驱动

看看 TodoItem、TodoList、TodoInput:

  • 全是纯函数组件,只接收 props,零状态。
  • TodoItem:checkbox + span(带 completed 类) + 删除按钮。
  • TodoInput:受控输入 + submit 添加。
  • TodoList:map todos 生成 TodoItem。

这种“聪明 Hook + 哑组件”模式,是 Hooks 时代的最佳实践。组件只管渲染,逻辑全在 Hook。

五、自定义 Hooks 最佳实践总结

  1. 命名必须 use 开头:让 ESLint 插件识别。
  2. 只在顶层调用 Hook:别放循环、条件里。
  3. 逻辑分离:一个 Hook 专注一件事(如 useTodos 只管 todos)。
  4. 依赖数组写全:用 eslint-plugin-react-hooks 的 exhaustive-deps 规则自动检查。
  5. 尽量少用 useEffect:能用纯状态逻辑就别用 effect。
  6. 复杂状态用 useReducer:todos 操作多时,可改用 reducer 更清晰。
  7. 复用优先:相同逻辑多个组件用?抽 Hook!

六、结语:Hooks 是前端团队的核心资产

通过这个 Todo List 项目,我们看到 Hooks 如何让代码从“乱麻”变成“丝滑”。自定义 Hooks 就像乐高积木,组合出无限可能。它不只是语法糖,而是提升代码质量、团队协作的利器。

下次写组件时,问自己:这个逻辑能不能抽成 Hook?多练几次,你会爱上这种函数式优雅!