本章定位
上一章,我们已经把记账板推进到了一个很关键的阶段。
你已经完成了这些非常重要的能力:
- 可以录入一条账单记录。
- 可以把记录渲染成列表。
- 可以切换“全部 / 收入 / 支出”筛选。
- 可以根据账单数据计算统计卡片。
- 开始真正理解多个组件如何围绕同一份状态协作。
也就是说,到现在为止,这个项目已经不再只是“能显示一些内容”,而是已经具备了:
表单、列表、筛选、统计这些 React 小项目最核心的基础骨架。
但如果你真的把它当成一个能用的小工具继续往下想,很快就会遇到几个非常现实的问题:
- 录错了一条记录,怎么修改?
- 某条记录不需要了,怎么删除?
- 页面一刷新,为什么数据全没了?
这三个问题非常有代表性。
因为它们说明你开始从“会写页面功能”进入到了另一个更接近真实项目的阶段:
让项目不仅能交互,还能被持续使用。
所以这一章,我们会正式把记账板继续推进到:
编辑记录、删除交互和本地存储接入后的更完整版本。
本章学习目标
学完这一章后,你应该能做到:
- 理解为什么编辑、删除和本地存储很适合放在同一阶段实现。
- 知道新增和编辑为什么通常共用同一个表单。
- 学会为项目补充
FormMode和editingRecordId相关状态。 - 掌握“进入编辑模式”和“切回新增模式”的基本思路。
- 学会区分“新增记录”和“更新记录”的处理差异。
- 理解删除交互本质上仍然是在更新原始列表状态。
- 学会给列表项接入编辑和删除按钮回调。
- 知道为什么
localStorage很适合用来做当前阶段的数据持久化。 - 理解为什么把数据写入本地存储是一种副作用。
- 学会使用
useState初始化函数恢复本地数据。 - 学会使用
useEffect在列表变化后自动保存数据。 - 建立“原始状态变化 -> 列表、筛选、统计、本地存储一起联动”的完整项目意识。
一、这一篇要把项目推进到哪一步
上一章我们打通的是:
录入 -> 新增 -> 列表渲染 -> 筛选统计
这一章要继续补齐的是:
编辑 -> 删除 -> 持久化保存
更具体地说,这一篇希望你真正做出来这些能力:
- 点击某条记录上的“编辑”按钮后,表单可以回填原内容。
- 用户修改后提交,页面更新原记录,而不是新增一条重复记录。
- 点击“删除”按钮后,记录可以从列表中移除。
- 页面刷新后,之前录入的记录仍然保留下来。
这一步非常关键。
因为从这里开始,你会明显感觉到:
这个项目开始从“练习页面”往“可持续使用的小工具”方向走了。
二、为什么编辑、删除和本地存储很适合放在同一阶段
这三件事看起来像不同类型的问题:
- 编辑更像表单问题
- 删除更像列表问题
- 本地存储更像浏览器能力问题
但它们其实有一个共同点:
它们都在围绕“同一份原始记录数据”工作。
例如:
- 编辑是在修改某条已有记录。
- 删除是在移除某条已有记录。
- 本地存储是在保存当前整份记录列表。
也就是说,这三件事虽然表面不同,但本质上都在处理:
recordList这份原始数据怎样变化、怎样保留下来。
所以把它们放在同一阶段,非常适合你建立一种更整体的感觉:
新增、编辑、删除、持久化,其实都是围绕同一份页面数据在做不同方向的更新。
三、先补上这一篇会用到的两个重要状态
当前阶段,我们先给页面补两条很关键的状态。
exporttypeFormMode="create"|"edit";const [formMode, setFormMode] = useState<FormMode>("create"); const [editingRecordId, setEditingRecordId] = useState<number | 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.FormEvent<HTMLFormElement>) { 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 className="record-actions"> <button type="button" onClick={function () { onEdit(record); }} > 编辑 </button> <button type="button" className="danger-button" onClick={function () { onDelete(record.id); }} > 删除 </button> </div>1. 这段代码真正说明了什么
它说明:
列表组件并不自己修改页面最终状态,而是通过回调把用户动作告诉父层。
2. 这和上一章学的什么一脉相承
这本质上仍然是:
父传子 + 子回调父
只是现在这个通信不再是简单的筛选按钮,而是已经落到真实的业务操作里了。
十八、为什么本地存储值得在这一章接进来
当你已经有了:
- 新增
- 编辑
- 删除
这三个围绕recordList的核心更新动作后,本地存储就特别适合接进来了。
为什么?
因为如果没有持久化,用户会明显感受到一个问题:
页面一刷新,前面做的所有操作都像没发生过一样。
这会让项目非常像“临时演示”,而不是“可以继续使用的小工具”。
而当前阶段的记账板,非常适合用最轻量的方式解决这个问题:
用
localStorage保存账单列表。
十九、为什么把数据写进localStorage属于副作用
这一点也非常值得和你前面学过的useEffect联系起来。
当前页面真正的核心状态是:
recordList而把它写到浏览器的localStorage里,本质上是在做什么?
在 React 渲染之外,把页面数据同步到外部环境。
这正是副作用的典型特征。
所以你可以把这一节和前面的 React 基础章节连起来理解:
列表状态是原始数据,写本地存储是副作用,所以更适合放进
useEffect。
二十、先准备一个稳定的存储键名
这是一个很小、但很值得养成的习惯。
不要在代码里到处手写同一个字符串。
更稳的做法是先准备一个常量:
exportconstACCOUNT_BOARD_STORAGE_KEY="stage-04-account-board-record-list";1. 为什么这一步值得做
因为后面你至少会在两个地方用到它:
- 读取本地数据
- 保存本地数据
2. 当前阶段最值得先记住什么
只要某个关键字符串会被多个地方反复使用,就值得先集中定义。
这能减少很多小错误。
二十一、为什么localStorage不能直接保存数组和对象
这一点在前面的 JavaScript 项目里你已经接触过一次,这里我们再回到 React 项目里重新理解它。
localStorage最直接适合保存的是:
字符串
所以如果你想把账单数组存进去,就需要先把它转成字符串。
例如:
localStorage.setItem(ACCOUNT_BOARD_STORAGE_KEY,JSON.stringify(recordList));而读取出来时,则要再转回来:
constsavedValue=localStorage.getItem(ACCOUNT_BOARD_STORAGE_KEY);constparsedValue=savedValue?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[]{constsavedValue=localStorage.getItem(ACCOUNT_BOARD_STORAGE_KEY);if(!savedValue){return[];}try{constparsedValue=JSON.parse(savedValue)asRecordItem[];// 只做最基础的结构兜底,避免解析失败直接让页面崩掉returnArray.isArray(parsedValue)?parsedValue:[];}catch{return[];}}1. 为什么这里要用try / catch
因为本地存储里的内容并不一定永远可信。
例如:
- 用户曾经手动改过
- 旧版本数据结构不一致
- 内容不是合法 JSON
如果不做保护,解析失败时页面就可能直接报错。
2. 当前阶段先做到什么程度就够了
先做到:
解析成功就恢复,解析失败就安全回退为空数组
这已经是一个很稳的起点。
二十三、再用useState的初始化函数恢复数据
这一节非常实用。
当前阶段一个很推荐的写法是:
const [recordList, setRecordList] = useState<RecordItem[]>(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 className="app-shell"> <header className="hero"> <p className="eyebrow">第四阶段综合实战</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 应用框架的下一步认知。