从混乱到清晰:项目架构与自定义 Hook 的双重实践

告别"胖组件",拥抱高内聚、可复用的工程化思维

你是否也经历过这样的场景:一个看似简单的 Todo 应用,其状态逻辑却像一团乱麻,分散在组件的各个生命周期和事件处理函数中?useStateuseEffect 和复杂的 JSX 逻辑交织在一起,让组件变得臃肿且难以维护。

说白了,这不仅仅是代码组织的问题,更是架构思想的缺失。

在 React 的世界里, "逻辑与视图分离" 是永恒的追求。而要实现这一点,仅靠组件自身的拆分是远远不够的,我们还需要合理的项目架构恰当的逻辑抽象 。今天,我们将结合一个典型的 React + TypeScript 项目结构,从零开始构建一个 Todo 应用,重点剖析其架构设计,并一步步抽离核心业务逻辑,封装成高内聚、可复用的 useTodos Hook。

读完这篇文章,你将收获:

  1. 一个清晰、可落地的 React 项目架构方案
  2. 封装自定义 Hook 的完整思路与最佳实践
  3. 将"架构思想"转化为实际代码的能力

一、先看骨架:一个"长得好"的项目应该长什么样?

在动手写代码之前,我们先来审视一个健康的 React + TypeScript 项目应该具备怎样的目录结构。这就像建房子,地基和框架决定了上层建筑能盖多高。

以下是一个经过实战检验的目录结构:

python 复制代码
src/
├── api/                # 🔌 所有与后端交互的接口请求
│   ├── client.ts       # axios/fetch 实例封装
│   └── todo.ts         # Todo 相关的 API 请求函数
├── assets/             # 🎨 静态资源(图片、字体、全局样式变量等)
│   ├── images/
│   └── styles/
├── components/         # 🧩 可复用的 UI 组件
│   ├── TodoFilter/     # 每个组件独立文件夹,包含其专属样式和测试
│   │   ├── TodoFilter.tsx
│   │   ├── TodoFilter.module.css
│   │   └── index.ts
│   ├── TodoInput/
│   ├── TodoList/
│   └── TodoItem/
├── hooks/              # 🪝 自定义 Hooks(逻辑复用的核心)
│   ├── useTodos.ts     # Todo 业务逻辑
│   └── useLocalStorage.ts # 通用工具 Hook
├── types/              # 📝 全局类型定义(数据蓝图)
│   ├── todo.ts
│   └── common.ts
├── utils/              # 🔧 工具函数库(纯函数)
│   ├── format.ts
│   └── validator.ts
├── App.tsx             # 应用根组件(组合各部分的胶水层)
├── App.css
├── main.tsx            # 应用入口
└── index.css           # 全局样式

为何要这样组织? 这背后的核心思想是 "关注点分离"

  • api/ :负责数据来源,不管是请求后端接口还是本地存储,都收敛于此。组件不需要关心数据从哪来,只需要调用 API 函数即可。
  • components/ :纯 UI 层,只关心"长什么样"和"用户操作如何触发事件",不包含任何业务逻辑和数据状态管理。这也符合原子设计理论,便于组合和测试。
  • hooks/ :业务逻辑的"大脑"。它是连接数据(API/状态)和视图(组件)的桥梁,也是我们实现逻辑复用的利器。
  • types/ :TypeScript 的威力所在。统一维护类型定义,能有效避免类型"飘散"在项目的各个角落,让协作和维护变得轻松。

"好的架构不是一蹴而就的,而是随着对业务理解的加深,不断演进出来的。" 但一个清晰的起点,决定了项目能走多远。

二、定义我们的"数据蓝图"(Types)

在动手写逻辑之前,我们先要明确数据的形状。TypeScript 是我们坚实的后盾,它能帮我们在编译阶段就避免大量低级错误。

/src/types/todo.ts 文件中,我们定义好 Todo 项和过滤条件的类型:

typescript 复制代码
// src/types/todo.ts
export type FilterType = 'all' | 'active' | 'completed';

export interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

有了这份清晰的"数据蓝图",我们的后续开发将更加稳健和高效。

三、将"大脑"装进 Hook(核心逻辑抽离)

现在,我们开始实现核心的业务逻辑 ------ useTodos Hook。它就像一个独立的"大脑",掌管着所有 Todo 数据的变化和派生。

typescript 复制代码
// src/hooks/useTodos.ts
import { useState } from 'react';
import type { Todo, FilterType } from '../types/todo';

export function useTodos() {
  // 1. 定义状态:数据源和过滤条件
  const [todos, setTodos] = useState<Todo[]>([]);
  const [filter, setFilter] = useState<FilterType>('all');

  // 2. 定义状态更新函数(业务逻辑)
  const addTodo = (text: string) => {
    if (!text.trim()) return; // 防御性编程,过滤空输入
    const newTodo: Todo = {
      id: Date.now().toString(),
      text: text.trim(),
      completed: false,
    };
    // ✅ 关键点:使用函数式更新,确保基于最新状态
    setTodos((prev) => [...prev, newTodo]);
  };

  const toggleTodo = (id: string) => {
    setTodos((prev) =>
      prev.map((item) =>
        item.id === id ? { ...item, completed: !item.completed } : item
      )
    );
  };

  const deleteTodo = (id: string) => {
    setTodos((prev) => prev.filter((item) => item.id !== id));
  };

  const clearCompleted = () => {
    setTodos((prev) => prev.filter((item) => !item.completed));
  };

  // 3. 计算派生状态(Derived State)
  // 这是一个计算属性,根据当前 filter 和 todos 实时计算出要展示的列表
  const filteredTodos = todos.filter((todo) => {
    if (filter === 'active') return !todo.completed;
    if (filter === 'completed') return todo.completed;
    return true; // 'all'
  });

  // 4. 返回"大脑"的接口,供视图层使用
  return {
    todos,          // 原始数据
    filteredTodos,  // 处理后的数据,直接用于渲染
    filter,
    setFilter,      // 暴露设置过滤条件的方法
    addTodo,
    toggleTodo,
    deleteTodo,
    clearCompleted,
  };
}

这里有三个关键的设计细节,值得你细细品味:

  1. 函数式更新 (setTodos(prev => ...)) : 这是保证状态更新安全性的最佳实践。它确保我们的更新操作基于的是最新的状态值,有效避免了因异步更新或批处理导致的"过时闭包"问题。
  2. 派生状态的计算 : 我们将 filteredTodos 的计算逻辑从组件中移到了 Hook 内部。这使得组件层只需要消费数据,而无需关心数据是如何被过滤的,逻辑更集中。
  3. 清晰的接口设计: Hook 返回的对象明确地暴露了视图层需要的数据和操作函数,形成了一个清晰的内聚单元。

四、在组件中"即插即用"

有了 useTodos 这个强大的 Hook,我们的视图组件(App.tsx)变得极其简洁、清爽。它只负责两件事:渲染 UI响应用户操作

javascript 复制代码
// src/App.tsx
import { useTodos } from './hooks/useTodos';
import { TodoList } from './components/TodoList';
import { TodoFilter } from './components/TodoFilter';
import './App.css';

function App() {
  // ✅ 一行代码,引入所有业务逻辑
  const {
    filteredTodos,
    filter,
    setFilter,
    addTodo,
    toggleTodo,
    deleteTodo,
    clearCompleted,
  } = useTodos();

  return (
    <div className="todo-app">
      <h1>我的待办</h1>
      {/* 输入框组件,调用 addTodo */}
      <TodoInput onAdd={addTodo} />
      {/* 列表组件,仅负责渲染 filteredTodos 并触发操作 */}
      <TodoList
        todos={filteredTodos}
        onToggle={toggleTodo}
        onDelete={deleteTodo}
      />
      {/* 底部操作栏 */}
      <TodoFooter
        filter={filter}
        onFilterChange={setFilter}
        onClearCompleted={clearCompleted}
      />
    </div>
  );
}

export default App;

组件从"如何管理数据"的复杂逻辑中解耦 出来,变得更加纯净易于测试。你可以很轻松地为一个组件替换不同的数据源或逻辑实现,只要它们符合相同的 Hook 接口。

五、架构如何演进?聊聊 API 层的插入

回到我们一开始的架构图,如果现在要求 Todo 数据来自后端 API,而不是本地状态,我们的架构会如何应对?

答案是:只需修改 useTodos Hook 内部实现,App.tsx 和其他 UI 组件完全不用动。

javascript 复制代码
// src/hooks/useTodos.ts
import { useState, useEffect } from 'react';
import { fetchTodos, createTodo, updateTodo, deleteTodo } from '../api/todo';
// ... 其余逻辑

export function useTodos() {
  const [todos, setTodos] = useState<Todo[]>([]);
  // ... filter 状态

  // 组件挂载时拉取数据
  useEffect(() => {
    fetchTodos().then(setTodos);
  }, []);

  // addTodo 改为调用 API
  const addTodo = async (text: string) => {
    if (!text.trim()) return;
    const newTodo = await createTodo({ text: text.trim(), completed: false });
    setTodos((prev) => [...prev, newTodo]);
  };

  // toggleTodo、deleteTodo 也做类似改造...
  // 但返回给组件的方法名和参数保持不变!
}

你会发现,架构的分层带来了极强的灵活性。UI 层、逻辑层、数据层各司其职,修改其中一层,其他层不会受到牵连。

总结:从 Todo 到任意业务逻辑

今天我们以一个标准的 React + TypeScript 项目结构为起点,完成了一次从架构设计自定义 Hook 封装的完整实战演练。

  • 合理的项目架构api / components / hooks / types)是团队协作和项目长期维护的基石。
  • 自定义 Hook 则是我们实现逻辑复用和视图解耦的核心武器。

这个小而美的重构,不仅仅是代码行数的减少,更是一次设计思想的升级。它让你的代码结构更清晰,逻辑更内聚,为后续的迭代和协作打下了坚实的基础。

从今天开始,当你在组件中写下第一个 useState 时,不妨问自己一句:"这部分逻辑,是不是可以放到一个 Hook 里?" 当你在 src 下新建文件夹时,想一想:"这个文件,放在哪个目录下最能体现它的职责?"

你的代码,乃至你的未来,都会感谢你。

相关推荐
@insist1232 小时前
系统集成项目管理工程师-架构基础与系统架构
架构·系统架构·软考·系统集成项目管理工程师·软考中项·软件水平考试
breeze jiang3 小时前
React + WebGPU 在浏览器运行 DeepSeek:从 Worker 通信到流式生成
前端·javascript·react.js
Dr.kangder3 小时前
嵌入式处理器虚拟化仿真技术(九)——SimpleScalar原理与应用
嵌入式硬件·架构·嵌入式·虚拟化·仿真
星火跨境XINGHUOS3 小时前
独立站建站技术栈全景拆解:从小白到能自己建站需要学什么?
架构
小林ixn3 小时前
前端卡顿终结者:用 useRef 把 Web Worker 请进 React 项目
前端·react.js·前端框架
sunly_3 小时前
React 三个重要概念:Ref、Props、State 详解
前端·javascript·react.js
Dr.kangder6 小时前
嵌入式处理器架构解析(十)——DSP+FPGA
fpga开发·架构·嵌入式
ltl12 小时前
高可用设计模式:冗余、故障转移与仲裁
架构
Dawson Zhu13 小时前
基于Palantir Foundry构建半导体制造AI友好型数据中台:从良率分析场景谈起
人工智能·语言模型·架构·制造·agi