本章定位
上一章,我们已经开始接触更接近真实项目的一类 React 能力:
- 组件加载后要做什么。
- 数据请求为什么属于副作用。
useEffect、状态和渲染之间怎么分工。
也就是说,到现在为止,你已经不只是会写静态组件,也不只是会做基础交互,而是已经开始能理解:
一个 React 页面是怎么围绕状态、事件、请求和渲染一起工作的。
这一步非常重要。
但如果你继续往下写,很快就会遇到一个新的现实问题:
页面功能越来越多以后,代码开始变乱了。
比如一个稍微真实一点的“学生信息管理页面”,可能就会同时有这些东西:
- 输入框表单
- 新增和编辑两种模式
- 提交校验
- 列表展示
- 搜索筛选
- 提交成功或失败提示
- 加载中状态
这时候很多初学者会开始出现这些感受:
- 状态太多了,不知道该放哪里。
- 表单值、错误信息、提交状态混在一起,看着很乱。
- 所有 JSX 都堆在一个组件里,越写越长。
- 文件一多,就不知道该怎么拆。
这说明你已经进入了 React 学习里一个非常关键的阶段:
不只是会写功能,还要开始学会整理功能。
所以这一章要解决的核心问题就是:
- React 表单页面通常怎么组织?
- 多状态页面怎样整理得更清楚?
- 小项目的组件、目录和文件,怎样拆分才更稳?
你可以把这一章理解成:
从“会写 React 功能”,走到“开始学会整理 React 小项目”。
本章学习目标
学完这一章后,你应该能做到:
- 理解 React 表单页面为什么容易变复杂。
- 回顾并巩固受控组件在表单场景中的作用。
- 知道一个表单页面里常见会出现哪些状态。
- 学会区分表单值状态、界面状态和派生结果。
- 理解多状态页面为什么需要整理。
- 知道什么时候适合拆成多个组件。
- 掌握最基础的项目目录分层思路。
- 理解页面组件、通用组件、工具函数、类型定义分别适合放在哪里。
- 知道什么是自定义 Hook 的雏形,以及什么时候值得提炼。
- 建立“先清楚,再抽象”的工程意识。
- 避开这一阶段最常见的几个坑。
- 为第四阶段综合实战做好结构和思维准备。
一、为什么 React 表单页面特别容易变乱
表单页面看起来往往不像列表页那样“复杂显眼”,但它非常容易在不知不觉中变乱。
为什么?
因为表单页面通常会把很多问题同时堆在一起:
- 输入值要管理
- 输入变化要响应
- 提交前要校验
- 提交时要有状态
- 提交后要清空或回填
- 编辑模式和新增模式可能还不同
也就是说,一个表单页面里,常常同时存在:
- 表单值
- 错误提示
- 模式切换
- 提交状态
- 列表刷新
这些内容一旦全塞在一个组件里,又没有整理意识,就会非常容易出现:
功能能跑,但越往后越不想改。
所以这一章的重点,不是告诉你“表单有多神秘”,而是帮你建立一个更现实的认知:
表单复杂,不是因为 React 特别难,而是因为它本来就同时牵涉状态、事件、校验、界面和结构组织。
二、先把主线拉直:React 表单的核心仍然是受控组件
虽然这一章会讲结构整理,但最底层的表单主线并没有变。
你前面已经学过:
受控组件 =
value由状态提供,onChange负责更新状态。
这一点依然是 React 表单的核心基础。
例如:
import { useState } from "react"; function UserNameInput() { const [userName, setUserName] = useState(""); return ( <input value={userName} onChange={function (event) { setUserName(event.target.value); }} placeholder="请输入姓名" /> ); }1. 这一章为什么还要再提受控组件
因为很多表单页面之所以会乱,不是因为不会写受控组件,而是因为:
一旦输入框从 1 个变成 5 个、8 个、10 个,很多人就开始不知道该怎么组织了。
所以这一章不是推翻前面的写法,而是要帮你从“会写一个输入框”继续往前走到:
会整理一整个表单页面。
三、一个稍微真实的表单页面,通常会有哪些状态
这一节非常重要。
因为很多初学者一看到状态变多,就会慌。
其实第一步不是急着优化,而是先把状态分类看清楚。
先拿一个“学生信息表单页面”举例,它可能会有这些状态:
1. 表单值状态
例如:
studentNameageclassNamephone
这些状态最直接地对应输入框内容。
2. 提交过程状态
例如:
isSubmitting
它表示:
当前是不是正在提交。
3. 错误提示状态
例如:
errorMessagefieldErrors
它们表示:
当前哪里有问题,应该给用户什么提示。
4. 页面模式状态
例如:
formMode = "create" | "edit"
它表示:
当前是在新增,还是在编辑。
5. 列表或结果状态
例如:
studentList
它表示:
当前页面上已经存在的数据集合。
6. 这一节最重要的结论是什么
不是所有状态都属于同一类。
你要开始慢慢建立的意识是:
状态一多时,先不要慌,先问自己:这些状态分别在表达哪一类问题?
这一步会直接决定你后面能不能整理清楚。
四、先学会区分三类内容:表单值、界面状态、派生结果
这一节是本章最关键的整理意识之一。
你可以先把表单页面里的数据粗略分成三类。
五、第一类:表单值状态
这一类最直观,就是用户正在输入或已经输入的内容。
例如:
- 姓名
- 手机号
- 班级
- 备注
这类数据通常会直接绑定在输入框上。
1. 它们最常见的特点是什么
- 来自用户输入
- 会随着输入变化
- 会参与提交
2. 这一类通常适合放哪里
当前阶段最常见的做法就是:
放在当前表单组件自己的状态里。
六、第二类:界面状态
这一类不一定是“用户要提交给后端的值”,但它会影响界面表现。
例如:
isSubmittingisOpenselectedTabisEditMode
1. 它和表单值有什么区别
例如一个“保存中…”提示,它不是用户真正输入的数据,但它会影响:
- 按钮是否禁用
- 页面上要不要显示加载提示
所以它属于界面状态,而不是表单字段本身。
2. 为什么这个区分重要
因为很多页面之所以越来越乱,就是把:
- 输入值
- 提交过程
- 弹窗开关
全都当成同一类东西在处理。
一旦你开始分清它们,页面结构就会明显更清楚。
七、第三类:派生结果
这一类特别值得提前认识。
所谓派生结果,你可以先理解成:
它不是原始状态,而是根据已有状态算出来的结果。
例如:
- 是否表单有效
- 当前已填写字段数量
- 当前展示标题
- 根据搜索词过滤后的列表
1. 为什么这类内容很重要
因为初学者很容易把所有东西都塞成状态。
但实际上,有些结果完全可以:
根据已有状态直接算出来。
例如:
const isFormValid = userName.trim() !== "" && className.trim() !== "";这就是一个典型的派生结果。
2. 当前阶段最值得先记住什么
如果某个值只是根据已有状态直接算出来,而且没有必要单独保存历史,那么你可以先想一想:
它是不是根本不用单独做成状态?
这个习惯会帮你避免很多不必要的状态膨胀。
八、多个输入框时,状态应该怎么放
这一节是很多初学者真正会卡住的地方。
因为一个输入框时很简单,多个输入框时就开始纠结:
我应该每个字段一个
useState,还是放在一个对象里?
当前阶段你可以先建立一个非常稳的认知:
没有唯一正确答案,重点是“相关内容放一起,结构保持清楚”。
九、方式一:每个字段一个状态
这是最直观、也最容易上手的方式。
例如:
import { useState } from "react"; function StudentForm() { const [studentName, setStudentName] = useState(""); const [age, setAge] = useState(""); const [className, setClassName] = useState(""); return <p>学生表单</p>; }1. 这种方式的优点是什么
- 非常直观
- 每个字段含义清楚
- 初学阶段最容易读懂
2. 它适合什么场景
当前阶段很适合:
- 字段数量不多
- 页面逻辑还比较简单
- 你更在意可读性而不是抽象度
对纯小白来说,这通常是一个非常稳的起点。
十、方式二:把相关字段收进一个对象状态
当字段开始变多时,有些人会考虑把表单值放进同一个对象。
例如:
import { useState } from "react"; function StudentForm() { const [formValue, setFormValue] = useState({ studentName: "", age: "", className: "" }); return <p>学生表单</p>; }1. 这种方式适合什么场景
更适合:
- 这些字段天然属于同一个表单对象
- 你经常需要整体拿到这组值
- 你希望“表单值”这一层概念更完整
2. 当前阶段需要注意什么
不是说“对象状态更高级”,也不是说“拆开写就落后”。
你现在最应该记住的是:
选择哪种方式,不看炫不炫,而看当前页面是否更清楚。
十一、当前阶段怎么选更稳
如果你现在还拿不准,可以先按这个顺序来判断:
- 字段少、逻辑简单时,先用“每个字段一个状态”
- 字段明显属于同一份表单对象时,可以考虑“对象状态”
- 无论哪种方式,只要代码开始变乱,就说明需要整理了
当前阶段最重要的不是追求一种“标准答案”,而是建立:
状态组织应该服务于可读性。
这句话非常关键。
十二、表单校验应该怎么想
表单页面一旦进入真实一点的场景,就一定会碰到校验。
例如:
- 姓名不能为空
- 年龄必须是数字
- 手机号格式不对
- 分数字段不能超过 100
很多人第一次写校验时,容易直接把所有判断都散落在事件里、提交里、渲染里,最后越来越乱。
当前阶段更稳的思路是:
先把“校验规则”和“界面展示”分开理解。
1. 校验规则在做什么
它本质上是在回答:
当前输入值合不合格?
2. 界面展示在做什么
它本质上是在回答:
如果不合格,我要怎么把提示告诉用户?
把这两层分开,你的代码就会清楚很多。
十三、一个简单的表单校验例子
例如:
function validateStudentForm(formValue) { if (formValue.studentName.trim() === "") { return "请输入学生姓名"; } if (formValue.className.trim() === "") { return "请输入班级名称"; } return ""; }1. 这个函数的价值是什么
它在做一件很重要的整理工作:
把“校验规则”从提交事件里提出来。
这样你后面在提交时就可以更清楚地写:
const errorMessage = validateStudentForm(formValue);2. 为什么这一步很重要
因为表单页面最容易出现的问题之一就是:
逻辑全挤在
handleSubmit里。
一旦把校验单独提出来,可读性会明显变好。
十四、不要把所有逻辑都塞进handleSubmit
这是本章非常想提前帮你建立的一个习惯。
很多初学者写表单时,最自然的路径就是:
所有事都放进提交函数里。
例如在一个handleSubmit里同时做:
- 阻止默认行为
- 读表单值
- 校验
- 改错误提示
- 调接口
- 改按钮状态
- 清空表单
这些当然都可能和提交有关,但如果全挤在一起,函数很快就会失控。
1. 更稳的做法是什么
可以尝试拆成几层:
- 一个函数负责收集或整理数据
- 一个函数负责校验
- 提交函数负责串流程
2. 当前阶段最重要的不是拆得多花
而是先建立这个意识:
表单提交流程可以有分层,不必所有事情都硬塞在一个函数里。
十五、一个稍微完整一点的表单页面思路
先看一个精简的页面思路,不追求一次把所有代码写满。
StudentPage ├── StudentForm ├── StudentList └── MessageBox这个结构很值得你慢慢看。
它说明了一件很重要的事:
表单页面并不一定是“一个大组件包打天下”,而可以拆成几个职责明确的部分。
1.StudentForm负责什么
例如:
- 输入框
- 提交按钮
- 表单校验提示
2.StudentList负责什么
例如:
- 渲染学生列表
- 展示基础信息
- 提供编辑入口
3.MessageBox负责什么
例如:
- 显示成功提示
- 显示失败提示
- 显示普通状态说明
这个拆分思路背后最核心的一句就是:
组件拆分要围绕职责,而不是围绕代码行数。
十六、什么时候值得把页面拆成多个组件
这一节很重要。
因为很多人会走到两个极端:
- 什么都不拆,全堆在一个页面组件里
- 什么都拆,结果目录一下子炸开
更稳的判断方式可以先抓这三个问题:
- 这块界面职责是否独立?
- 这块逻辑是否值得单独理解?
- 这块结构是否可能复用或长期维护?
1. 适合拆的常见部分
例如:
- 表单区
- 列表区
- 搜索栏
- 结果提示区
- 通用按钮
2. 当前阶段最稳的节奏
你不需要一上来就拆得特别细。
先让结构更清楚,再考虑进一步抽象。
这是比“先追求完美分层”更稳的路线。
十七、项目目录为什么也需要整理
当你开始写的不是单个练习组件,而是一个小项目时,很快就会发现另一个问题:
文件也会变乱。
比如你可能一开始只有:
App.tsxstyle.css
这当然没问题。
但一旦页面开始有:
- 表单组件
- 列表组件
- 类型定义
- 工具函数
- 自定义 Hook
如果还全堆在一起,后面会越来越难找、越来越难维护。
所以目录整理不是形式主义,而是在解决一个很实际的问题:
当项目文件增多时,怎么让结构仍然好找、好改、好理解。
十八、一个适合初学者的小项目目录示意
下面给你一个当前阶段非常够用、也比较容易理解的目录结构:
src/ ├── components/ │ ├── StudentForm.tsx │ ├── StudentList.tsx │ └── MessageBox.tsx ├── hooks/ │ └── useStudentForm.ts ├── types/ │ └── student.ts ├── utils/ │ └── validateStudent.ts ├── App.tsx └── main.tsx1.components/一般放什么
放页面里可复用或职责清楚的组件。
例如:
- 表单组件
- 列表组件
- 按钮组件
- 提示组件
2.hooks/一般放什么
放提炼出来的自定义 Hook。
例如:
- 表单状态管理逻辑
- 搜索逻辑
- 倒计时逻辑
3.types/一般放什么
放类型定义。
尤其是在 TypeScript 项目里,这会非常有价值。
例如:
StudentItemStudentFormValueMessageType
4.utils/一般放什么
放和界面无关、但会被多处复用的普通函数。
例如:
- 校验函数
- 格式化函数
- 数据转换函数
5. 当前阶段最重要的不是死记目录名
而是理解每个目录在回答什么问题:
- 这是组件吗?
- 这是 Hook 吗?
- 这是类型吗?
- 这是普通工具函数吗?
只要这个分类意识建立起来,你就会越来越稳。
十九、页面组件和通用组件怎么区分
这一点也很实用。
很多初学者在拆组件时,容易把所有组件都看成一个层级。
但实际上,页面组件和通用组件往往职责并不一样。
1. 页面组件更像“组织页面的人”
例如:
StudentPageTaskDashboardArticleListPage
它们更常负责:
- 组织状态
- 串联数据流
- 组合多个子组件
2. 通用组件更像“完成局部界面的人”
例如:
ButtonMessageBoxEmptyStateModal
它们更常负责:
- 某一块具体界面的展示
- 根据
props做局部呈现
3. 当前阶段先建立什么意识就够了
你现在先记住一句很够用的话:
页面组件更像“总控”,通用组件更像“零件”。
这会帮助你后面做目录整理和职责划分。
二十、什么是自定义 Hook 的雏形
“自定义 Hook”这个词第一次看起来可能有点吓人。
但当前阶段你完全不用把它理解成什么高级魔法。
你现在可以先把它理解成:
把一段重复出现的状态逻辑和相关操作,提炼成一个可复用函数。
这个函数通常以use开头。
例如:
useStudentFormuseSearchKeyworduseTimer
1. 为什么这件事会出现
因为当页面越来越真实后,你会慢慢发现:
有些重复的不是 JSX,而是状态和逻辑本身。
例如:
- 输入框状态管理逻辑
- 搜索关键词管理逻辑
- 表单重置逻辑
这时候就开始有了提炼自定义 Hook 的价值。
二十一、一个非常基础的自定义 Hook 雏形
先看一个很简单的例子:
import { useState } from "react"; function useTextField(initialValue = "") { const [value, setValue] = useState(initialValue); function handleChange(event) { setValue(event.target.value); } function reset() { setValue(initialValue); } return { value, handleChange, reset }; }1. 这个例子想说明什么
它想说明:
有时候我们提炼的不是“按钮长什么样”,而是“输入框状态怎么管理”。
2. 当前阶段最重要的不是马上大量写 Hook
而是先建立一个判断:
如果一段状态逻辑在多个地方都很像,就值得考虑是不是能提炼出来。
这已经是很好的起点了。
二十二、什么时候值得提炼自定义 Hook
当前阶段你可以先抓住三个判断标准:
- 这段状态逻辑是不是重复出现了?
- 这段逻辑是不是和具体 JSX 结构关系不大?
- 提炼之后会不会让代码更清楚?
1. 不要为了“会 Hook”而硬提炼
这是特别重要的一点。
很多人学到自定义 Hook 后,会下意识觉得:
只要能提炼,我就该提炼。
这通常不是最稳的思路。
2. 当前阶段更稳的原则
你可以先记一句话:
先重复,再抽象;先清楚,再提炼。
这句话对后面做项目特别有帮助。
二十三、一个小型表单页面的结构化示意
下面给你一个更接近真实项目的思路示意。
StudentPage ├── 状态:studentList / formValue / formMode / message / isSubmitting ├── 逻辑:validateStudentForm / handleSubmit / handleEdit / handleDelete ├── 组件:StudentForm / StudentList / MessageBox └── 工具:formatScore / createStudentItem1. 这个结构最想说明什么
它最想说明的是:
一个页面开始变复杂时,不要只盯着 JSX,而要开始从“状态、逻辑、组件、工具”四层去看它。
2. 为什么这会让你轻松很多
因为你一旦能这样看代码,就不会再只是觉得:
这文件怎么这么长。
你会开始更有条理地判断:
- 哪部分是页面状态
- 哪部分是业务逻辑
- 哪部分是界面组件
- 哪部分值得拆出去
这就是工程意识开始建立的地方。
二十四、状态整理时,一个很重要的原则:相关的放一起,不相关的分开
这一点非常简单,但非常管用。
很多页面之所以乱,不是因为状态太多,而是因为状态之间的关系没有被看清楚。
你可以先这样理解:
1. 相关的状态,应该尽量靠近
例如:
- 表单值相关的状态
- 提交过程相关的状态
- 列表筛选相关的状态
这些内容各自可以形成自己的小块。
2. 不相关的状态,不要硬揉在一块
例如:
- 弹窗是否打开
- 搜索关键词
- 表单错误信息
如果这些完全不同的问题被胡乱混着处理,可读性就会明显下降。
3. 当前阶段最实用的判断方式
每当你觉得页面状态开始变多时,先问自己一句:
这些状态是在解决同一个问题,还是根本是几类不同问题?
这个问题会帮你非常快地理清结构。
二十五、不要为了抽象而抽象
这是本章最想提前帮你建立的一种节奏感。
很多初学者在学到组件拆分、Hook、工具函数、目录结构之后,会出现一个很自然但也很危险的倾向:
我是不是应该把一切都抽出来?
答案通常不是。
1. 为什么过早抽象会有问题
因为它很容易带来这些情况:
- 文件数量突然变很多
- 命名越来越抽象
- 来回跳文件更痛苦
- 自己过几天回来都看不懂
2. 当前阶段更稳的路线是什么
你可以先按这个顺序来:
- 先把功能写清楚
- 看哪里已经明显重复
- 看哪里已经明显过长
- 再做小步整理
这就是一种非常稳的“先清楚,再抽象”的节奏。
二十六、初学者最容易踩的几个坑
这一节建议你认真看。
因为第三十二章其实不只是在学语法,而是在学整理能力。
1. 坑一:所有状态全堆在一起,没有分类意识
这样写到后面时,你会越来越难判断:
- 哪个状态是表单值
- 哪个状态是错误提示
- 哪个状态是界面模式
2. 坑二:所有 JSX 都堆在一个页面组件里
这在最早期练习可以接受,但只要页面稍微真实一点,就会很难维护。
3. 坑三:为了“高级感”,过早拆很多文件
看起来像是“结构化”了,实际上可能只是把复杂度挪到了别处。
4. 坑四:把派生结果也强行塞成状态
如果某个值完全可以由现有状态直接算出来,就先别急着单独给它开状态。
5. 坑五:一学到 Hook,就什么逻辑都想提炼
自定义 Hook 很有价值,但它不是为了证明“我会 Hook”,而是为了:
减少重复,让逻辑更清楚。
二十七、本章实践练习
这一章的练习重点,是把“表单、状态、组件和目录”这些零散知识真正串到一起。
1. 练习 1:完成一个带新增和校验的学生表单
请你写一个学生表单,至少包含:
- 姓名
- 班级
- 分数
并完成这些功能:
- 输入框使用受控组件
- 提交前做基础校验
- 校验失败时显示提示
这个练习的重点是:
把表单值和校验逻辑分开组织。
2. 练习 2:给表单增加“新增 / 编辑”两种模式
请你尝试再加一个模式状态,例如:
createedit
并思考这些问题:
- 页面标题是否要跟模式切换?
- 按钮文案是否要变化?
- 提交逻辑是否有差别?
这个练习会帮助你真正感受到:
页面一旦进入多状态场景,整理意识为什么这么重要。
3. 练习 3:整理一个小项目目录
请你把当前 React 小项目至少整理成这些结构:
components/utils/types/
如果你状态比较稳,还可以继续尝试:
hooks/
这个练习的重点不是目录名本身,而是训练你建立:
文件职责分类的意识。
4. 练习 4:尝试提炼一个自定义 Hook 雏形
你可以从这些方向里任选一个:
- 输入框状态管理
- 搜索关键词管理
- 表单重置逻辑
不需要追求复杂,只要做到:
- 把重复逻辑提出来
- 命名清楚
- 提炼后代码更好懂
这个练习会帮你开始建立:
“先重复,再抽象”的实际手感。
二十八、学习重点提示
这一章请你重点记住下面这些话:
- React 表单页面之所以容易变乱,是因为它常常同时包含输入、校验、模式、提交和列表联动等多类问题。
- 受控组件依然是 React 表单的核心基础。
- 多状态页面首先要做的,不是急着优化,而是先把状态分类看清楚。
- 可以先区分表单值状态、界面状态和派生结果。
- 状态组织没有唯一标准,重点是让当前页面更清楚。
- 页面组件、通用组件、工具函数、类型定义和 Hook,职责通常并不一样。
- 目录整理不是形式主义,而是在文件增多后维持可读性和可维护性。
- 自定义 Hook 的价值在于提炼重复逻辑,而不是为了“显得高级”。
- 先清楚,再抽象;先重复,再提炼。
- 不要为了抽象而抽象,先保证当前结构易懂。
如果你只记一句话,请记住:
React 小项目写到后面,真正决定你轻不轻松的,往往不是会不会某个 API,而是你会不会整理状态、组件和文件结构。
二十九、本章小结
这一章,我们正式把 React 从“会写页面功能”推进到了“开始整理页面结构和项目结构”。
你已经理解了:
- React 表单页面为什么容易复杂起来
- 表单页面里常见的几类状态分别在表达什么
- 为什么要区分表单值、界面状态和派生结果
- 多输入框时状态可以怎样组织
- 校验逻辑为什么值得从提交流程里分离出来
- 页面组件和通用组件的职责区别
- 小项目目录为什么需要分层
- 自定义 Hook 的雏形是什么,以及什么时候值得提炼
更重要的是,你开始建立了一种非常关键的工程感:
功能能跑只是第一步,结构清楚、状态清楚、文件职责清楚,项目才会越来越好维护。
这一步非常关键。
因为从这里开始,你已经不只是会写 React 练习题,而是在开始进入:
真实小项目的整理与演进阶段。
三十、课后思考题
请你认真思考下面这些问题:
- 为什么 React 表单页面比很多人想象中更容易变乱?
- 为什么说先把状态分类看清楚,比一上来就优化更重要?
- 表单值状态、界面状态和派生结果,这三类内容最基础的区别是什么?
- 多输入框时,“每个字段一个状态”和“对象状态”分别适合什么情况?
- 为什么校验逻辑值得从
handleSubmit里往外提? - 页面组件和通用组件的职责,最基础的区别是什么?
- 为什么说目录整理本质上是在解决“文件越来越多以后怎么保持清楚”这个问题?
- 为什么“先清楚,再抽象;先重复,再提炼”是一个更稳的节奏?
建议你把这些问题用自己的话写下来。
只要你能把这些问题讲清楚,说明你已经真正开始进入 React 小项目组织的主线了。
三十一、下一篇预告
接下来我们会进入第四阶段综合实战:
第四阶段综合实战:记账板或任务管理面板
到那时,你会把这一阶段学过的核心内容真正串起来:
- JSX 与组件拆分
Props与State- 列表渲染与条件渲染
- 事件处理
useEffect与数据请求- 表单组织与状态整理
也就是说,接下来我们不再只讲单点知识,而是开始真正做一个:
具备列表、筛选、表单和统计信息的 React 中小型交互项目。