🐭 用 React Hooks “钓”住鼠标,顺便写个 TodoList —— 初学者也能看懂的自定义 Hook 实战指南!

36 阅读4分钟

“React Hooks 不是魔法,但用好了,你就是魔法师。”


大家好!今天不聊八股文,不背面试题,咱们来点实战又有趣的——用 React Hooks 写一个能实时追踪鼠标位置的小功能,再顺手搭个清爽的待办事项(TodoList)应用。
更重要的是:我们还要自己封装两个自定义 Hook,让代码像乐高一样可复用、可组合、还贼优雅!

准备好了吗?Let’s Hook it up! 🎣


一、Hooks 是什么?为什么它让我爱上函数组件?

在 React 的远古时代(其实也就几年前),想管理状态和副作用?不好意思,请用 class 组件,写一堆 this.setState 和生命周期方法。

直到 React 16.8 带着 Hooks 闪亮登场——
函数组件也能拥有状态、副作用、甚至“超能力”!

  • useState:给函数组件加状态
  • useEffect:处理副作用(比如发请求、监听事件)
  • useContext:跨组件传数据
  • ……以及,你可以自己造轮子useYourOwnMagic()

而所有内置或自定义的 Hook 都有个不成文的规矩:名字必须以 use 开头
这不只是约定,更是 React 的“咒语识别机制”——看到 useXxx,就知道:“哦,这是个 Hook,我要特殊对待。”


二、实战第一弹:用 useMouse 实时追踪鼠标位置 🖱️

想象一下:用户移动鼠标,页面上立刻显示坐标 (x, y)。听起来简单?但别忘了——内存泄漏的陷阱就在脚下!

❌ 错误示范(新手常踩坑):

// 千万别这么写!
useEffect(() => {
  const handleMove = (e) => {
    setX(e.clientX);
    setY(e.clientY);
  };
  window.addEventListener('mousemove', handleMove);
  // 忘了 removeEventListener?组件卸载后监听器还在跑!
}, []);

当组件被销毁(比如切换页面),这个监听器不会自动消失。它会继续默默运行,占用内存,甚至可能操作已卸载组件的状态——React 会警告你:“Can't perform a React state update on an unmounted component.”

✅ 正确姿势:用 useEffect 的返回函数清理副作用!

// hooks/useMouse.js
import { useState, useEffect } from 'react';

export function useMouse() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMove = (e) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };

    window.addEventListener('mousemove', handleMove);

    // 👇 关键!返回一个清理函数
    return () => {
      window.removeEventListener('mousemove', handleMove);
    };
  }, []); // 空依赖,只在挂载/卸载时执行

  return position;
}

现在,在任何组件里,只要一行代码就能“钓”到鼠标:

function MouseMove() {
  const { x, y } = useMouse();
  return <div>🐭 鼠标位置:{x}, {y}</div>;
}

干净、安全、可复用! 这就是自定义 Hook 的魅力。


三、实战第二弹:用 useTodos 管理待办事项 📝

接下来,我们挑战更复杂的逻辑:一个完整的 TodoList,包含添加、删除、完成/未完成切换。

如果把这些逻辑全塞进 App 组件?代码会迅速膨胀成“意大利面条”。
不如——把它抽成 useTodos Hook!

// hooks/useTodos.js
import { useState } from 'react';

export function useTodos() {
  const [todos, setTodos] = useState([
    { id: 1, text: '学习 React Hooks', completed: false }
  ]);

  const addTodo = (text) => {
    const newTodo = {
      id: Date.now(), // 简单 ID 生成(生产环境请用 uuid)
      text,
      completed: false
    };
    setTodos([...todos, newTodo]);
  };

  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
  };
}

看!状态 + 操作逻辑 全封装在一个 Hook 里。
App 中使用时,简直清爽到飞起:

// App.jsx
import { useTodos } from './hooks/useTodos';
import TodoInput from './components/TodoInput';
import TodoList from './components/TodoList';

export default function App() {
  const { todos, addTodo, toggleTodo, deleteTodo } = useTodos();

  return (
    <>
      <h1>✨ 我的待办清单</h1>
      <TodoInput addTodo={addTodo} />
      {todos.length > 0 ? (
        <TodoList
          todos={todos}
          onToggle={toggleTodo}
          onDelete={deleteTodo}
        />
      ) : (
        <p>🎉 恭喜!没有待办事项了~</p>
      )}
    </>
  );
}

每个子组件只负责 UI 渲染,逻辑全交给 Hook——关注点分离,代码可读性拉满!


四、为什么自定义 Hook 是 React 开发者的“瑞士军刀”?

  1. 复用逻辑,不是 UI
    Mixin 和 HOC 复用的是组件结构,而 Hook 复用的是状态逻辑本身。比如 useMouse 可以用在任何需要追踪鼠标的场景。
  2. 避免“Wrapper Hell”
    不再需要层层嵌套的高阶组件,代码扁平如草原。
  3. 测试友好
    自定义 Hook 可以单独测试(配合 @testing-library/react-hooks),不用渲染整个组件树。
  4. 组合优于继承
    你可以把多个 Hook 组合起来:useMouse + useLocalStorage + useDebounce……像搭积木一样构建复杂功能。

五、小彩蛋:条件渲染 + Hook 的“坑”

注意这段被注释掉的代码:

{count % 2 === 0 && <MouseMove />}

如果取消注释,你会发现:每次 count 变化导致 MouseMove 卸载/重挂,鼠标位置会重置为 (0,0)

为什么?因为 useMouseuseEffect 在组件卸载时清理了监听器,重挂时重新初始化状态。

这不是 Bug,而是符合预期的行为!
但也提醒我们:不要随意条件渲染带状态的组件,除非你真的希望它“重置”。


六、结语:Hook 之道,在于“组合”与“克制”

React Hooks 强大,但不是万能胶水。
好的 Hook 应该:

  • 职责单一(比如 useMouse 只管鼠标位置)
  • 命名清晰(一看就知道用途)
  • 自动处理副作用清理(防内存泄漏!)
  • 不过度抽象(别为了 Hook 而 Hook)

最后送大家一句话:

“当你觉得某个逻辑在多个组件里重复出现,就是时候写个 useXXX 了。”