🏗️ 写完 Todos 之后大型 React 项目的 个架构真相
📌 前言:痛点先说
当你把 Todo 项目迁移到真正的业务程序时常见的痛点是:
-
文件夹层级太浅,组件堆积如山——找不到目标文件。
-
状态混在一起,导致耦合度高、测试困难。其实,
-
Hooks 写在“哪里”让人摸不着头脑——到底放在业务模块还是共享层?
-
路由和权限处理缺失,直接暴露给未登录使用者。
-
代码质量与可维护性并非只靠单元测试,还需要规范化工具链。
样子的?"
⚠️ 这篇文章不是纯理论堆砌。每章都有真实踩坑案例
🎯 这篇文章适合谁
-
已掌握 React 基础,却想了解生产环境下的项目结构。
-
会写自定义 Hooks,却不知 Hooks 在大项目中如何拆分。
-
准备面试或进阶,希望补齐「工程化」短板。
📚 一、先看看我们的 Todos 项目长啥样
todos-ts-hooks/
├── src/
│ ├── types/
│ │ └── todo.ts # 类型定义
│ ├── hooks/
│ │ └── useTodos.ts # 自定义 Hook
│ ├── components/
│ │ ├── TodoInput.tsx
│ │ ├── TodoItem.tsx
│ │ ├── TodoList.tsx
│ │ └── TodoFilter.tsx
│ ├── App.tsx # 根组件
│ └── main.tsx # 入口文件
├── package.json
└── vite.config.ts
# types/todo.tsexport interface Todo { id:string;text:string,completed:boolean;}export type FilterType='all'|'completed'|'uncompleted';
# hooks/useTodos.tsimport {useState}from'react';怎么说呢,import type{Todo。FilterType}from'../types/todo';export function useTodos{const =useState;const =useState;const addTodo==>{if)return;const newTodo:{id:string;text:string,不过,completed:boolean}= {id:Date.now.toString。text:text.trim,completed:false};话说回来,setTodos;},const toggleTodo==>{setTodos);},const deleteTodos==>{setTodos);},const clearCompleted==>{setTodos);},return{todos,filter,setFilter,addTodo。toggleTodo,deleteTodos,clearCompleted};}
This simple structure suffices for learning but crumbles when scaling up—think multi‑user auth。multiple lists,persistence.
📂 二、目录结构:大型项目的第一道防线
说到小项目痛点,按类型平铺导致查找效率低下。
Migrating a few components into a folder tree works until you have dozens of files in /components/.
说到真实踩坑,平铺式目录让新手翻半天找文件。
推荐结构
src/
├─ app/ # 应用级配置
├─ features/ # ⭐业务功能模块
│ ├─ auth/ # 认证模块
│ │ ├─ components/
│ │ │ ├─ LoginForm.tsx
│ │ │ └─ RegisterForm.tsx
│ │ ├─ hooks/
│ │ ...
└─ shared/ # ⭐公共层
...
└─ assets/
└─ styles/
...
| 原则 | 说明 |
| 高内聚 / 就近原则 / 单一职责 / 接口统一导出 |
🪝 三、自定义 Hooks:从能用到好用
. 单一职责原则 —— 把 useTodos 拆成多块:
import {useState}from'react';说起来,import type{Todo}from '../types/todo';export function useTodoList{const =useState;/* CRUD */ return{todos,...};}
// hooks/useTodoFilter.ts
import {useState,useMemo}from'react';export function useTodoFilter{/* filter logic */}
// hooks/useTodos.ts
import {useTodoList}from'./useTodoList';import {useTodoFilter}from'./useTodoFilter';export function useTodos{/* combine */}
. 真正踩坑:Hook 拆分过头导致调用方复杂。
// ❌ 多 Hook 调用
const {data,setField}=useFormData;
const {errors}=useFormValidation;,// ✅ 合理拆分
const {data,errors}=useForm;
. 通用 vs. 业务 Hooks
// shared/hooks/useDebounce.ts
export function useDebounce{…说起来,}
// features/todo/hooks/useTodos.ts — 业务逻辑封装…
| 类型 | 命名模式 | 示例 |
| 状态管理 | `use`+名词 `useTodos` `useUser` | |
| 副作用 | `use`+动词 `useFetch` `useSubscribe` | |
-
"容器组件处理数据、业务;其实,展示组件只负责渲染。" — 常见误区是让展示组件直接发请求。"
-
"复合组件模式" — 当子组件共享上下文时用 Context + 子组件组合实现。"
-
"不要让容器和展示边界模糊,否则测试难以隔离。" — 示例代码展示错误与正确做法。"
-
"React.memo 用于避免不必要重渲染,但过度使用无意义。" — 提醒仅对渲染开销大的列表项使用。"
"
function TodoList{
const { todos,toggleDelete } = useTodos;return (
{todos.map(t=>(
toggleDelete} />
))}
);}
function MemoizedItem {
return memo{
console.log;return -
{todo.text}
});}
// 错误做法:展示里直接 fetch 删除 API
function Item {
const handleDelete = async =>{
await fetch;// side effect...
};return ,}
**复合组件示例**:
tsx
…实现通过 Context 与子 `