从零开始学前端 | 第三十二章:React 表单、状态整理与项目结构
本章定位上一章我们已经开始接触更接近真实项目的一类 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. 这一类通常适合放哪里当前阶段最常见的做法就是放在当前表单组件自己的状态里。六、第二类界面状态这一类不一定是“用户要提交给后端的值”但它会影响界面表现。例如isSubmittingisOpenselectedTabisEditMode1. 它和表单值有什么区别例如一个“保存中…”提示它不是用户真正输入的数据但它会影响按钮是否禁用页面上要不要显示加载提示所以它属于界面状态而不是表单字段本身。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 项目里这会非常有价值。例如StudentItemStudentFormValueMessageType4.utils/一般放什么放和界面无关、但会被多处复用的普通函数。例如校验函数格式化函数数据转换函数5. 当前阶段最重要的不是死记目录名而是理解每个目录在回答什么问题这是组件吗这是 Hook 吗这是类型吗这是普通工具函数吗只要这个分类意识建立起来你就会越来越稳。十九、页面组件和通用组件怎么区分这一点也很实用。很多初学者在拆组件时容易把所有组件都看成一个层级。但实际上页面组件和通用组件往往职责并不一样。1. 页面组件更像“组织页面的人”例如StudentPageTaskDashboardArticleListPage它们更常负责组织状态串联数据流组合多个子组件2. 通用组件更像“完成局部界面的人”例如ButtonMessageBoxEmptyStateModal它们更常负责某一块具体界面的展示根据props做局部呈现3. 当前阶段先建立什么意识就够了你现在先记住一句很够用的话页面组件更像“总控”通用组件更像“零件”。这会帮助你后面做目录整理和职责划分。二十、什么是自定义 Hook 的雏形“自定义 Hook”这个词第一次看起来可能有点吓人。但当前阶段你完全不用把它理解成什么高级魔法。你现在可以先把它理解成把一段重复出现的状态逻辑和相关操作提炼成一个可复用函数。这个函数通常以use开头。例如useStudentFormuseSearchKeyworduseTimer1. 为什么这件事会出现因为当页面越来越真实后你会慢慢发现有些重复的不是 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 中小型交互项目。

相关新闻