不是哥们?别再把逻辑塞进组件了!用自定义 Hooks 优雅管理 Todo 项目

143 阅读3分钟
  • 前言(共鸣 + 痛点)
  • 核心原理(为什么要自定义 Hooks)
  • 实战拆解(useTodos 怎么写)
  • 高级进阶(alias、useContext 怎么优化)
  • 总结(关键点)
  • 彩蛋互动(拉评论)

“逻辑越来越多,组件越来越臃肿…”
如果你也在用 React 写 ToDo 应用,肯定遇到过:

  • 状态管理写来写去,setState 到处飞
  • 组件动不动几百行,根本找不到重点
  • 重复逻辑没法复用,改个小需求全局牵一发而动全身

别怕,这一切都能用 自定义 Hooks 优雅解决!
咱今天就拿 useTodos 拆给你看,手把手教你:

  • 怎么把逻辑单独拎出来
  • 怎么保持状态持久化(localStorage)
  • 怎么组合 useState + useEffect
  • 怎么让组件更纯粹,只管渲染

1️⃣ 自定义 Hooks 是啥?为啥一定要用?

先别急着上来就写。先搞懂自定义 Hooks 到底是啥?

一句话理解:
自定义 Hooks = use开头 的函数,用来把组件里用到的状态逻辑提取出来,封装成可复用的函数。

它满足几个特点:

  • ✅ 一定是 useXXX 开头(不然 React 检测不到)
  • ✅ 内部可以用 useStateuseEffectuseReduceruseContext 等等内置 Hook
  • ✅ 可以返回值(状态)和函数(操作逻辑)
  • ✅ 在组件里像普通函数一样调用

这样做的好处:

  • 让组件只关注「模板渲染」
  • 逻辑复用度高,换一个页面直接拿来用
  • 管理复杂状态变得可控

2️⃣ 看一个最典型例子:useTodos

假如现在要写一个 Todo 应用,要支持:

  • ✅ 添加 todo
  • ✅ 切换完成状态
  • ✅ 删除 todo
  • ✅ 本地持久化(关掉页面也不丢)

看完整代码

import { useState, useEffect } from 'react';

export const useTodos = () => {
  // 1️⃣ 初始化,从 localStorage 读
  const [todos, setTodos] = useState(
    JSON.parse(localStorage.getItem('todos')) || []
  );

  // 2️⃣ 监听 todos,每次变化都存到 localStorage
  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]);

  // 3️⃣ 新增 todo
  const addTodo = (text) => {
    setTodos([
      ...todos,
      {
        id: Date.now(),
        text,
        isComplete: false
      }
    ]);
  };

  // 4️⃣ 切换 todo 状态
  const onToggle = (id) => {
    setTodos(
      todos.map(todo =>
        todo.id === id
          ? { ...todo, isComplete: !todo.isComplete }
          : todo
      )
    );
  };

  // 5️⃣ 删除 todo
  const onDelete = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  // 6️⃣ 统一暴露
  return {
    todos,
    addTodo,
    onToggle,
    onDelete
  };
};

这里面包含了几个重点

  1. useState 保存 todo 列表,状态和组件分离
  2. useEffect 监听 todos 变化,实时同步到 localStorage
  3. 通过返回对象,把增、删、改的操作都暴露给外部
  4. 组件用的时候,直接 const { todos, addTodo } = useTodos(),调用就行

3️⃣ 真实场景下,这样封装有多香?

假如现在写个 TodoList 组件

import { useTodos } from '@/hooks/useTodos';

export default function TodoList() {
  const { todos, addTodo, onToggle, onDelete } = useTodos();

  return (
    <div>
      <button onClick={() => addTodo('新待办')}>新增</button>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input 
              type="checkbox" 
              checked={todo.isComplete} 
              onChange={() => onToggle(todo.id)} 
            />
            {todo.text}
            <button onClick={() => onDelete(todo.id)}>删除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

✨ 组件本身就专注做一件事:渲染
状态怎么来、怎么存、怎么切换,交给 useTodos 负责。


4️⃣ 高级场景优化:两点可别忽略!

你这段笔记里提到的两个“遗憾”,是真的常见


遗憾 1:路径山路十八弯

import { useTodos } from '../../../hooks/useTodos';

一旦层级深,就特别丑:

../../../../../../hooks/useTodos

解决方案:Vite + alias
vite.config.js 里加一条:

import { defineConfig } from 'vite';
import path from 'path';

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  }
});

以后随便写:

import { useTodos } from '@/hooks/useTodos';

遗憾 2:操作跨层太麻烦

有时候 onToggleonDelete 要传好几层 props,非常绕。

解决方案:useContext 做全局状态共享

import { createContext, useContext } from 'react';

// 创建上下文
export const TodosContext = createContext(null);

// 封装一个 Provider
export const TodosProvider = ({ children }) => {
  const todosHook = useTodos();
  return (
    <TodosContext.Provider value={todosHook}>
      {children}
    </TodosContext.Provider>
  );
};

// 在任意子组件中:
export const useTodosContext = () => useContext(TodosContext);

以后子孙组件直接 useTodosContext(),不再层层 props 传递,简直不要太爽!


5️⃣ 写在最后:再也别把逻辑写死在组件里!

写到这儿,咱复盘下:

  • 自定义 Hooks 本质就是把状态 + 副作用提出来
  • use开头、内部可用 useStateuseEffect 等 Hook
  • 搭配 alias,告别丑路径
  • 需要跨层通信,配 useContext 完美联动

彩蛋互动

你平时会自己手写自定义 Hooks 吗?
有没有遇到跨组件层级很烦人的场景?
评论区告诉我,顺便点个赞支持一下,让更多人别再写“屎山组件”!