本章定位上一章我们已经把记账板推进到了一个很关键的阶段。你已经完成了这些非常重要的能力可以录入一条账单记录。可以把记录渲染成列表。可以切换“全部 / 收入 / 支出”筛选。可以根据账单数据计算统计卡片。开始真正理解多个组件如何围绕同一份状态协作。也就是说到现在为止这个项目已经不再只是“能显示一些内容”而是已经具备了表单、列表、筛选、统计这些 React 小项目最核心的基础骨架。但如果你真的把它当成一个能用的小工具继续往下想很快就会遇到几个非常现实的问题录错了一条记录怎么修改某条记录不需要了怎么删除页面一刷新为什么数据全没了这三个问题非常有代表性。因为它们说明你开始从“会写页面功能”进入到了另一个更接近真实项目的阶段让项目不仅能交互还能被持续使用。所以这一章我们会正式把记账板继续推进到编辑记录、删除交互和本地存储接入后的更完整版本。本章学习目标学完这一章后你应该能做到理解为什么编辑、删除和本地存储很适合放在同一阶段实现。知道新增和编辑为什么通常共用同一个表单。学会为项目补充FormMode和editingRecordId相关状态。掌握“进入编辑模式”和“切回新增模式”的基本思路。学会区分“新增记录”和“更新记录”的处理差异。理解删除交互本质上仍然是在更新原始列表状态。学会给列表项接入编辑和删除按钮回调。知道为什么localStorage很适合用来做当前阶段的数据持久化。理解为什么把数据写入本地存储是一种副作用。学会使用useState初始化函数恢复本地数据。学会使用useEffect在列表变化后自动保存数据。建立“原始状态变化 - 列表、筛选、统计、本地存储一起联动”的完整项目意识。一、这一篇要把项目推进到哪一步上一章我们打通的是录入 - 新增 - 列表渲染 - 筛选统计这一章要继续补齐的是编辑 - 删除 - 持久化保存更具体地说这一篇希望你真正做出来这些能力点击某条记录上的“编辑”按钮后表单可以回填原内容。用户修改后提交页面更新原记录而不是新增一条重复记录。点击“删除”按钮后记录可以从列表中移除。页面刷新后之前录入的记录仍然保留下来。这一步非常关键。因为从这里开始你会明显感觉到这个项目开始从“练习页面”往“可持续使用的小工具”方向走了。二、为什么编辑、删除和本地存储很适合放在同一阶段这三件事看起来像不同类型的问题编辑更像表单问题删除更像列表问题本地存储更像浏览器能力问题但它们其实有一个共同点它们都在围绕“同一份原始记录数据”工作。例如编辑是在修改某条已有记录。删除是在移除某条已有记录。本地存储是在保存当前整份记录列表。也就是说这三件事虽然表面不同但本质上都在处理recordList这份原始数据怎样变化、怎样保留下来。所以把它们放在同一阶段非常适合你建立一种更整体的感觉新增、编辑、删除、持久化其实都是围绕同一份页面数据在做不同方向的更新。三、先补上这一篇会用到的两个重要状态当前阶段我们先给页面补两条很关键的状态。exporttypeFormModecreate|edit;const [formMode, setFormMode] useStateFormMode(create); const [editingRecordId, setEditingRecordId] useStatenumber | null(null);1.formMode在表达什么它表达的是当前表单到底是在“新增模式”还是在“编辑模式”。这非常重要。因为虽然表单长得还是那一个表单但它提交后的处理逻辑已经开始不一样了新增模式往数组里加入新对象编辑模式更新数组里原来的对象2.editingRecordId在表达什么它表达的是当前正在编辑的是哪一条记录。因为进入编辑模式后你总得知道要把哪条记录回填进表单最后提交时又该更新数组里的哪一项3. 为什么这两条状态值得一开始就单独立出来因为它们不是临时变量而是真正影响表单标题提交按钮文案提交流程列表更新目标这些页面行为的关键状态。四、为什么新增和编辑通常共用同一个表单这是本章最重要的认识之一。很多初学者一想到“编辑功能”第一反应是我是不是还得再做一个编辑表单当前阶段通常并不需要。因为新增和编辑收集的数据本质上往往还是同一套类型分类金额日期备注真正不同的地方不在“表单长什么样”而在提交后这份数据是生成一个新对象还是更新一个已有对象。1. 共用一个表单有什么好处页面结构更简单不需要维护两套几乎一样的输入区数据流更集中2. 当前阶段最值得先建立的意识你可以先记住一句很关键的话React 项目里很多时候不是“新增页面”和“编辑页面”本质不同而是同一个表单在不同模式下工作。五、先把“进入编辑模式”这条主线讲清楚当用户点击某条记录的“编辑”按钮时页面其实需要连续完成几件事进入编辑模式记住当前正在编辑的记录 ID把这条记录的内容回填进表单更新表单区域提示文案这几步一旦没想清楚编辑功能就很容易写乱。所以当前阶段最稳的方式不是“点哪里补哪里”而是先把这条流程看清楚。六、先写一个把记录回填成表单值的函数这一节非常实用。因为列表里的RecordItem和表单里的RecordFormValue并不是完全同一种形态。尤其是amount在业务对象里是数字在表单值里是字符串。所以先写一个转换函数会很顺。importtype{RecordFormValue,RecordItem}from../types/record;exportfunctionmapRecordToFormValue(record:RecordItem):RecordFormValue{return{type:record.type,category:record.category,amount:String(record.amount),date:record.date,note:record.note};}1. 为什么这里值得单独做一次映射因为它把一个很容易散落在事件逻辑里的转换动作单独封装了出来。这会让后面“进入编辑模式”的逻辑更清楚。2. 这一节最重要的不是记住函数名而是先建立这个意识列表对象和表单对象虽然相关但它们不一定应该直接混用。七、再写一个“进入编辑模式”的处理函数当你已经有了回填函数后编辑入口就会清楚很多。function handleStartEdit(record: RecordItem) { setFormMode(edit); setEditingRecordId(record.id); setFormValue(mapRecordToFormValue(record)); setMessage(正在编辑${record.category}修改后点击“保存修改”。); }1. 这个函数最核心的价值是什么它把“点击编辑后页面要进入什么状态”这件事一次性讲清楚了。2. 为什么这里直接传整条record因为编辑时我们不仅需要它的id还需要类型分类金额日期备注也就是说编辑按钮触发的不只是“定位目标”还包括“用这条记录重新填满表单”。八、为什么编辑模式一定要有“切回新增模式”的出口这是很多初学者第一次写编辑功能时很容易漏掉的一点。一旦页面进入编辑模式如果没有一个稳定的退出方式就会出现这些问题用户改到一半不想改了不知道怎么退回去。某条记录删掉后表单还停留在旧的编辑状态。一次保存完成后页面没有恢复到稳定的默认模式。所以当前阶段更稳的做法是明确准备一个“切回新增模式”的函数。九、先写switchToCreateMode这个函数会在很多场景里复用到用户主动取消编辑保存修改成功后删除当前正在编辑的记录后function switchToCreateMode(nextMessage: string 请继续录入新的账单记录。) { setFormMode(create); setEditingRecordId(null); setFormValue(createInitialFormValue()); setMessage(nextMessage); }1. 为什么这个函数特别值得保留因为它帮你统一了“页面如何回到稳定默认状态”这件事。2. 当前阶段你最该建立的意识不是只有“进入某种模式”需要函数退出某种模式、恢复稳定状态同样值得单独封装。十、先把“更新记录对象”这件事单独写出来新增记录时我们前面已经写过createRecordItem这一类逻辑。到了编辑阶段更稳的方式是再补一个根据旧记录和当前表单值生成更新后记录的函数。importtype{RecordFormValue,RecordItem}from../types/record;exportfunctionupdateRecordItem(targetRecord:RecordItem,formValue:RecordFormValue):RecordItem{return{id:targetRecord.id,type:formValue.type,category:formValue.category.trim(),amount:Number(formValue.amount),date:formValue.date,note:formValue.note.trim()};}1. 为什么这里一定保留原来的id因为编辑不是创造一条全新的业务记录而是更新原来那条记录的内容。如果这里把id也换掉列表里的“同一条记录”概念就会乱掉。2. 为什么编辑逻辑也值得单独做成函数因为这会让表单提交函数少承担一块纯对象处理的职责。换句话说提交流程负责串步骤对象转换负责做数据处理。十一、统一表单提交时怎么区分“新增”和“编辑”现在我们终于来到本章最核心的一段逻辑之一同一个表单提交时页面怎么知道这是新增还是编辑答案其实就是看formMode先看一个当前阶段很适合作为主线的写法function handleSubmitRecord(event: React.FormEventHTMLFormElement) { event.preventDefault(); const errorMessage validateRecordForm(formValue); if (errorMessage) { setMessage(errorMessage); return; } if (formMode edit editingRecordId ! null) { setRecordList(function (prevRecordList) { return prevRecordList.map(function (record) { return record.id editingRecordId ? updateRecordItem(record, formValue) : record; }); }); switchToCreateMode(账单记录已更新。); return; } const newRecord createRecordItem(formValue); setRecordList(function (prevRecordList) { return [newRecord, ...prevRecordList]; }); switchToCreateMode(账单记录已新增可以继续录入下一条。); }1. 这段代码最值得你先看懂什么先看清这一层就够了无论新增还是编辑提交前都先校验真正的分叉点在formMode编辑走map更新原数组新增走“往前插入新记录”2. 为什么这里编辑用的是map因为编辑的思路不是删除再新增而是保留其他所有记录只替换目标那一项。这正适合map。十二、为什么编辑成功后也要切回新增模式这一点非常重要。很多人第一次实现编辑功能时保存成功后会忘了处理页面状态。这时候就会出现表单里还是上一条数据按钮还显示“保存修改”用户下一次录入时不知道现在算新增还是继续改所以保存成功后更稳的节奏通常是更新原列表重置表单退出编辑模式给出新的提示文案这也正是switchToCreateMode()存在的重要价值。十三、删除交互本质上是在做什么现在来看删除功能。表面上看“删除”像是在做一个危险动作。但从数据角度看它的本质其实很简单重新生成一份“不包含当前这条记录”的新数组。这和我们前面在 JavaScript 阶段、待办事项项目里做过的思路是一样的。所以你可以先把删除功能想得很朴素不是“把界面里的一项抹掉”而是“更新页面真正的原始数据”。十四、先写handleDeleteRecord当前阶段很适合作为起点的写法如下function handleDeleteRecord(recordId: number) { setRecordList(function (prevRecordList) { return prevRecordList.filter(function (record) { return record.id ! recordId; }); }); if (editingRecordId recordId) { switchToCreateMode(正在编辑的记录已删除表单已恢复新增模式。); return; } setMessage(账单记录已删除。); }1. 为什么这里适合用filter因为删除的思路正好就是保留所有“不是当前这条”的记录。这本质上就是一次过滤。2. 为什么这里要额外判断editingRecordId因为如果你删掉的刚好就是当前正在编辑的那条记录那么页面就不能继续停留在旧的编辑状态。这时候更稳的处理一定是删除成功后同时把表单恢复回新增模式。十五、为什么删除和编辑都会影响整个页面联动这一步很值得你停一下想清楚。因为很多初学者做项目时容易把功能看成编辑只影响表单删除只影响列表但真实情况并不是这样。只要recordList一变至少这些东西都可能跟着变列表内容筛选结果统计卡片空状态判断本地存储这其实是在反复提醒你一件事页面真正的核心不是某个按钮做了什么而是原始状态变了以后整个页面如何一起跟着更新。十六、把编辑和删除真正接进RecordList现在我们把列表组件继续往前推进。这一版里RecordList除了展示记录外还要承担把“编辑”动作抛给父组件把“删除”动作抛给父组件先看 props 设计import type { RecordItem } from ../types/record; interface RecordListProps { recordList: RecordItem[]; emptyTitle: string; emptyDescription: string; onEdit: (record: RecordItem) void; onDelete: (recordId: number) void; }1. 为什么onEdit传整条记录因为编辑动作需要的不只是id还需要整条记录的完整内容来回填表单。2. 为什么onDelete传id就够了因为删除动作当前只需要知道要删除哪一条所以传recordId就足够了。这也是一个很好的细节意识组件通信时传递“刚刚够用”的数据通常会更清楚。十七、给列表项补上编辑和删除按钮当前阶段你可以先这样接到记录卡片里div classNamerecord-actions button typebutton onClick{function () { onEdit(record); }} 编辑 /button button typebutton classNamedanger-button onClick{function () { onDelete(record.id); }} 删除 /button /div1. 这段代码真正说明了什么它说明列表组件并不自己修改页面最终状态而是通过回调把用户动作告诉父层。2. 这和上一章学的什么一脉相承这本质上仍然是父传子 子回调父只是现在这个通信不再是简单的筛选按钮而是已经落到真实的业务操作里了。十八、为什么本地存储值得在这一章接进来当你已经有了新增编辑删除这三个围绕recordList的核心更新动作后本地存储就特别适合接进来了。为什么因为如果没有持久化用户会明显感受到一个问题页面一刷新前面做的所有操作都像没发生过一样。这会让项目非常像“临时演示”而不是“可以继续使用的小工具”。而当前阶段的记账板非常适合用最轻量的方式解决这个问题用localStorage保存账单列表。十九、为什么把数据写进localStorage属于副作用这一点也非常值得和你前面学过的useEffect联系起来。当前页面真正的核心状态是recordList而把它写到浏览器的localStorage里本质上是在做什么在 React 渲染之外把页面数据同步到外部环境。这正是副作用的典型特征。所以你可以把这一节和前面的 React 基础章节连起来理解列表状态是原始数据写本地存储是副作用所以更适合放进useEffect。二十、先准备一个稳定的存储键名这是一个很小、但很值得养成的习惯。不要在代码里到处手写同一个字符串。更稳的做法是先准备一个常量exportconstACCOUNT_BOARD_STORAGE_KEYstage-04-account-board-record-list;1. 为什么这一步值得做因为后面你至少会在两个地方用到它读取本地数据保存本地数据2. 当前阶段最值得先记住什么只要某个关键字符串会被多个地方反复使用就值得先集中定义。这能减少很多小错误。二十一、为什么localStorage不能直接保存数组和对象这一点在前面的 JavaScript 项目里你已经接触过一次这里我们再回到 React 项目里重新理解它。localStorage最直接适合保存的是字符串所以如果你想把账单数组存进去就需要先把它转成字符串。例如localStorage.setItem(ACCOUNT_BOARD_STORAGE_KEY,JSON.stringify(recordList));而读取出来时则要再转回来constsavedValuelocalStorage.getItem(ACCOUNT_BOARD_STORAGE_KEY);constparsedValuesavedValue?JSON.parse(savedValue):[];1. 为什么这里一定会出现JSON.stringify和JSON.parse因为数组和对象不能直接按你想象的结构放进localStorage。它们需要先被序列化成字符串再在读取时恢复结构。2. 这一步和 React 本身没有冲突React 管的是页面如何围绕状态更新而localStorage管的是浏览器如何把数据保存在本地它们正好可以接在一起。二十二、先写一个读取本地账单的函数当前阶段很适合作为起点的写法如下import{ACCOUNT_BOARD_STORAGE_KEY}from../constants/storage;importtype{RecordItem}from../types/record;exportfunctionloadSavedRecordList():RecordItem[]{constsavedValuelocalStorage.getItem(ACCOUNT_BOARD_STORAGE_KEY);if(!savedValue){return[];}try{constparsedValueJSON.parse(savedValue)asRecordItem[];// 只做最基础的结构兜底避免解析失败直接让页面崩掉returnArray.isArray(parsedValue)?parsedValue:[];}catch{return[];}}1. 为什么这里要用try / catch因为本地存储里的内容并不一定永远可信。例如用户曾经手动改过旧版本数据结构不一致内容不是合法 JSON如果不做保护解析失败时页面就可能直接报错。2. 当前阶段先做到什么程度就够了先做到解析成功就恢复解析失败就安全回退为空数组这已经是一个很稳的起点。二十三、再用useState的初始化函数恢复数据这一节非常实用。当前阶段一个很推荐的写法是const [recordList, setRecordList] useStateRecordItem[](function () { return loadSavedRecordList(); });1. 为什么这里用函数初始化而不是直接写值因为这样可以表达得更清楚第一次初始化状态时先从本地恢复数据2. 当前阶段你先怎么理解就够了你可以先把它理解成页面第一次建立recordList时不再从空数组开始而是优先看浏览器本地有没有之前保存过的数据。这就是刷新后还能保留记录的第一步。二十四、再用useEffect自动保存最新列表当recordList被新增、编辑或删除改变后我们就可以把它自动写回本地存储。useEffect(function () { localStorage.setItem( ACCOUNT_BOARD_STORAGE_KEY, JSON.stringify(recordList) ); }, [recordList]);1. 为什么这里非常适合useEffect因为它的语义非常清楚当账单列表状态变化后把最新结果同步到外部环境。这正是useEffect很适合做的事情。2. 这一段还有一个很好的副作用管理好处因为只要新增了记录编辑了记录删除了记录本质上都会更新recordList。而useEffect只盯着这一份原始状态就能统一完成保存动作。这会让代码结构非常清楚。二十五、为什么本地存储通常先只保存原始记录不保存筛选和统计结果这一点非常值得讲清楚。很多人第一次接本地存储时容易出现一个冲动页面上显示的东西这么多是不是都保存一下当前阶段通常没必要。更稳的思路是先只保存最原始、最核心、最难重新构造的数据。在这个项目里就是recordList而这些内容filteredRecordListsummaryData空状态文案本来就可以根据原始数据重新算出来。1. 这样做有什么好处存储结构更简单恢复页面时更清楚不容易出现多份结果不同步2. 这也再次印证了什么这再次印证了上一章最关键的一条思路原始状态和派生结果要尽量分开理解。二十六、如果当前正在编辑某条记录删除它时为什么要特别小心这一点看起来像边角问题但在真实项目里很重要。想象这样一个场景你点开了某条记录准备编辑表单已经回填了这条记录内容然后你又把这条记录删掉了这时候如果页面还继续停留在旧的编辑状态就会很怪表单里还显示已不存在的数据提交按钮还写着“保存修改”当前editingRecordId也已经失效所以更稳的处理一定是只要删掉的是当前正在编辑的记录就立刻切回新增模式。这就是为什么我们前面在handleDeleteRecord里专门加了那段判断。二十七、现在把这一章的App主体真正串起来到这里我们已经有了这些核心部分recordListformValuemessagefilterTypeformModeeditingRecordIdfilteredRecordListsummaryData所以App会越来越像一个真正的页面总控。例如const filteredRecordList getFilteredRecordList(recordList, filterType); const summaryData calculateSummaryData(recordList); return ( main classNameapp-shell header classNamehero p classNameeyebrow第四阶段综合实战/p h1记账板/h1 /header SummaryCards summaryData{summaryData} / FilterTabs activeFilter{filterType} onFilterChange{setFilterType} / RecordForm formMode{formMode} formValue{formValue} message{message} onFormChange{handleFormValueChange} onSubmit{handleSubmitRecord} onCancelEdit{switchToCreateMode} / RecordList recordList{filteredRecordList} emptyTitle{emptyStateConfig.title} emptyDescription{emptyStateConfig.description} onEdit{handleStartEdit} onDelete{handleDeleteRecord} / /main );1. 为什么这时的App比前几章更像“项目中心”因为它现在已经不仅在组织原始数据派生结果还在真正组织模式切换业务动作本地持久化2. 这其实是在练什么能力这其实是在练React 页面级状态组织和业务编排能力这对后面做更完整的小项目会非常关键。二十八、把这一条完整数据流翻译成人话这一节非常重要。因为只要你能把这条链路用自己的话讲清楚说明你已经真的开始理解这个项目而不是只是在复制代码。1. 新增时发生了什么发生的是用户输入表单表单值写进formValue提交后校验校验通过后生成新记录新记录进入recordList列表、统计、筛选结果和本地存储一起联动更新2. 编辑时发生了什么发生的是用户点击列表项的“编辑”页面进入编辑模式目标记录内容回填进表单提交后更新原数组中的那一项页面切回新增模式列表、统计和本地存储一起同步3. 删除时发生了什么发生的是用户点击“删除”原数组过滤掉目标记录页面相关展示全部重新计算如果删的是正在编辑的记录表单也一起恢复本地存储自动写入最新结果4. 这一章最关键的一句话是什么就是新增、编辑、删除看起来是不同功能但它们最终都在更新同一份原始记录状态而页面的其他部分会围绕这份状态一起联动。二十九、这一章最容易踩的几个坑这一节建议你认真看。因为项目一旦进入“编辑 删除 持久化”细节问题会明显变多。1. 坑一编辑时直接改原对象这会让数据变化边界变得很模糊。当前阶段更稳的做法仍然是通过状态更新返回新结果而不是直接在原对象上改字段。2. 坑二忘记区分新增模式和编辑模式这样用户点“保存修改”时你可能会错误地新增出一条重复记录。3. 坑三编辑成功后忘记切回新增模式这样页面会一直停留在旧的编辑状态里。4. 坑四删除当前编辑项后没有清理编辑状态这会让页面进入一种很别扭、也很容易出错的状态。5. 坑五本地存储里什么都想保存当前阶段先保存原始记录就够了。不要急着把派生结果也一起塞进去。6. 坑六读取本地数据时不做异常保护一旦 JSON 解析失败页面就可能直接报错。7. 坑七新增、编辑、删除之后只想着改列表不去想统计和存储一定要记住只要原始列表状态变了页面的其他依赖结果通常也会跟着一起变。三十、本章实践练习这一章的练习重点是把“模式切换、列表更新和持久化保存”真正练熟。1. 练习 1给表单补上“取消编辑”按钮请你在RecordForm里补出编辑模式才显示的“取消编辑”按钮点击后切回新增模式这个练习会帮助你真正理解编辑模式不只是能进入也必须能稳定退出。2. 练习 2让删除按钮先弹出确认提示你可以尝试在删除前先做一个简单确认window.confirm(确定要删除这条账单记录吗)这个练习会帮助你开始思考有些交互不仅是“能做”还要考虑用户是不是容易误操作。3. 练习 3手动刷新页面验证本地存储是否真的生效请你依次测试新增一条记录后刷新编辑一条记录后刷新删除一条记录后刷新这个练习的重点是真正确认“原始状态变化 - 本地保存 - 刷新恢复”这条链路已经跑顺。4. 练习 4给本地存储增加版本前缀或更清楚的键名例如尝试调整成from-zero-frontend-account-board-v1这个练习会帮助你建立存储键名也属于项目设计的一部分。三十一、学习重点提示这一章请你重点记住下面这些话编辑、删除和本地存储虽然表面不同但本质上都在围绕同一份原始记录状态工作。新增和编辑通常共用同一个表单不同的是提交后的处理逻辑。formMode和editingRecordId是编辑能力里非常关键的两条状态。删除一条记录本质上通常是在生成一份不包含它的新数组。把数据写进localStorage属于副作用所以很适合放在useEffect里。本地存储优先保存原始记录数据而不是筛选结果和统计结果。只要原始列表状态变化列表、统计、筛选结果和本地存储都会跟着联动。退出编辑模式同样是一个值得单独组织的页面行为。项目越往后做越要学会区分“原始状态”“派生结果”和“副作用”。如果你只记一句话请记住这一章真正要建立的不只是“会写编辑和删除”而是“会让同一份原始数据在页面、模式和本地存储之间稳定流动起来”。三十二、本章小结这一章我们正式把记账板从“可录入、可筛选、可统计”推进到了“更接近真实可用状态”的阶段。你已经理解了为什么新增和编辑通常共用同一个表单为什么编辑模式需要formMode和editingRecordId如何让某条记录回填进表单并进入编辑状态删除交互为什么本质上仍然是在更新原数组为什么把数据写入本地存储属于副作用如何通过useState初始化函数恢复数据如何通过useEffect自动保存最新列表为什么只保存原始记录数据通常会更稳更重要的是你开始真正建立一种很关键的项目感一个 React 小项目真正开始变得像“工具”往往不是因为页面更花而是因为数据能被修改、能被删除、能被保留下来。这一步非常关键。因为从这里开始你已经不只是在给项目加功能而是在真正把它推进到更接近真实使用场景的可持续状态。三十三、课后思考题请你认真思考下面这些问题为什么说编辑、删除和本地存储都在围绕同一份原始记录状态工作为什么新增和编辑通常共用一个表单而不是直接做两套输入区formMode和editingRecordId分别在解决什么问题为什么删除当前正在编辑的记录时页面需要额外清理编辑状态为什么把recordList写到localStorage很适合放进useEffect为什么本地存储通常先只保存原始记录而不是把筛选结果和统计数字也一起保存为什么说新增、编辑、删除虽然功能不同但最后都会推动整个页面一起联动建议你把这些问题用自己的话写下来。只要你能把这些问题讲清楚说明你已经真正开始进入 React 综合项目完整交互的主线了。三十四、下一篇预告接下来我们会正式结束 React 阶段综合实战并准备进入第五阶段的内容。下一章我们会进入从零开始学前端 | 第三十七章为什么学习 Next.js到那时你会开始理解这些问题Next.js 和 React 到底是什么关系为什么只会 React 还不够路由、工程能力和服务端能力为什么会变得重要也就是说下一章开始我们会从“React 小项目综合实战”继续走到现代 React 应用框架的下一步认知。