从零开始学前端 | 第三十六章:记账板编辑记录、删除交互与本地存储
2026/7/22 11:18:47 网站建设 项目流程

本章定位

上一章,我们已经把记账板推进到了一个很关键的阶段。

你已经完成了这些非常重要的能力:

  1. 可以录入一条账单记录。
  2. 可以把记录渲染成列表。
  3. 可以切换“全部 / 收入 / 支出”筛选。
  4. 可以根据账单数据计算统计卡片。
  5. 开始真正理解多个组件如何围绕同一份状态协作。

也就是说,到现在为止,这个项目已经不再只是“能显示一些内容”,而是已经具备了:

表单、列表、筛选、统计这些 React 小项目最核心的基础骨架。

但如果你真的把它当成一个能用的小工具继续往下想,很快就会遇到几个非常现实的问题:

  1. 录错了一条记录,怎么修改?
  2. 某条记录不需要了,怎么删除?
  3. 页面一刷新,为什么数据全没了?

这三个问题非常有代表性。

因为它们说明你开始从“会写页面功能”进入到了另一个更接近真实项目的阶段:

让项目不仅能交互,还能被持续使用。

所以这一章,我们会正式把记账板继续推进到:

编辑记录、删除交互和本地存储接入后的更完整版本。

本章学习目标

学完这一章后,你应该能做到:

  1. 理解为什么编辑、删除和本地存储很适合放在同一阶段实现。
  2. 知道新增和编辑为什么通常共用同一个表单。
  3. 学会为项目补充FormModeeditingRecordId相关状态。
  4. 掌握“进入编辑模式”和“切回新增模式”的基本思路。
  5. 学会区分“新增记录”和“更新记录”的处理差异。
  6. 理解删除交互本质上仍然是在更新原始列表状态。
  7. 学会给列表项接入编辑和删除按钮回调。
  8. 知道为什么localStorage很适合用来做当前阶段的数据持久化。
  9. 理解为什么把数据写入本地存储是一种副作用。
  10. 学会使用useState初始化函数恢复本地数据。
  11. 学会使用useEffect在列表变化后自动保存数据。
  12. 建立“原始状态变化 -> 列表、筛选、统计、本地存储一起联动”的完整项目意识。

一、这一篇要把项目推进到哪一步

上一章我们打通的是:

录入 -> 新增 -> 列表渲染 -> 筛选统计

这一章要继续补齐的是:

编辑 -> 删除 -> 持久化保存

更具体地说,这一篇希望你真正做出来这些能力:

  1. 点击某条记录上的“编辑”按钮后,表单可以回填原内容。
  2. 用户修改后提交,页面更新原记录,而不是新增一条重复记录。
  3. 点击“删除”按钮后,记录可以从列表中移除。
  4. 页面刷新后,之前录入的记录仍然保留下来。

这一步非常关键。

因为从这里开始,你会明显感觉到:

这个项目开始从“练习页面”往“可持续使用的小工具”方向走了。

二、为什么编辑、删除和本地存储很适合放在同一阶段

这三件事看起来像不同类型的问题:

  • 编辑更像表单问题
  • 删除更像列表问题
  • 本地存储更像浏览器能力问题

但它们其实有一个共同点:

它们都在围绕“同一份原始记录数据”工作。

例如:

  1. 编辑是在修改某条已有记录。
  2. 删除是在移除某条已有记录。
  3. 本地存储是在保存当前整份记录列表。

也就是说,这三件事虽然表面不同,但本质上都在处理:

recordList这份原始数据怎样变化、怎样保留下来。

所以把它们放在同一阶段,非常适合你建立一种更整体的感觉:

新增、编辑、删除、持久化,其实都是围绕同一份页面数据在做不同方向的更新。

三、先补上这一篇会用到的两个重要状态

当前阶段,我们先给页面补两条很关键的状态。

exporttypeFormMode="create"|"edit";
const [formMode, setFormMode] = useState<FormMode>("create"); const [editingRecordId, setEditingRecordId] = useState<number | null>(null);

1.formMode在表达什么

它表达的是:

当前表单到底是在“新增模式”,还是在“编辑模式”。

这非常重要。

因为虽然表单长得还是那一个表单,但它提交后的处理逻辑已经开始不一样了:

  • 新增模式:往数组里加入新对象
  • 编辑模式:更新数组里原来的对象

2.editingRecordId在表达什么

它表达的是:

当前正在编辑的是哪一条记录。

因为进入编辑模式后,你总得知道:

  • 要把哪条记录回填进表单
  • 最后提交时,又该更新数组里的哪一项

3. 为什么这两条状态值得一开始就单独立出来

因为它们不是临时变量,而是真正影响:

  • 表单标题
  • 提交按钮文案
  • 提交流程
  • 列表更新目标

这些页面行为的关键状态。

四、为什么新增和编辑通常共用同一个表单

这是本章最重要的认识之一。

很多初学者一想到“编辑功能”,第一反应是:

我是不是还得再做一个编辑表单?

当前阶段通常并不需要。

因为新增和编辑收集的数据,本质上往往还是同一套:

  • 类型
  • 分类
  • 金额
  • 日期
  • 备注

真正不同的地方,不在“表单长什么样”,而在:

提交后,这份数据是生成一个新对象,还是更新一个已有对象。

1. 共用一个表单有什么好处

  1. 页面结构更简单
  2. 不需要维护两套几乎一样的输入区
  3. 数据流更集中

2. 当前阶段最值得先建立的意识

你可以先记住一句很关键的话:

React 项目里,很多时候不是“新增页面”和“编辑页面”本质不同,而是同一个表单在不同模式下工作。

五、先把“进入编辑模式”这条主线讲清楚

当用户点击某条记录的“编辑”按钮时,页面其实需要连续完成几件事:

  1. 进入编辑模式
  2. 记住当前正在编辑的记录 ID
  3. 把这条记录的内容回填进表单
  4. 更新表单区域提示文案

这几步一旦没想清楚,编辑功能就很容易写乱。

所以当前阶段最稳的方式不是“点哪里补哪里”,而是先把这条流程看清楚。

六、先写一个把记录回填成表单值的函数

这一节非常实用。

因为列表里的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,还需要:

  • 类型
  • 分类
  • 金额
  • 日期
  • 备注

也就是说:

编辑按钮触发的不只是“定位目标”,还包括“用这条记录重新填满表单”。

八、为什么编辑模式一定要有“切回新增模式”的出口

这是很多初学者第一次写编辑功能时,很容易漏掉的一点。

一旦页面进入编辑模式,如果没有一个稳定的退出方式,就会出现这些问题:

  1. 用户改到一半不想改了,不知道怎么退回去。
  2. 某条记录删掉后,表单还停留在旧的编辑状态。
  3. 一次保存完成后,页面没有恢复到稳定的默认模式。

所以当前阶段更稳的做法是:

明确准备一个“切回新增模式”的函数。

九、先写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. 这段代码最值得你先看懂什么

先看清这一层就够了:

  1. 无论新增还是编辑,提交前都先校验
  2. 真正的分叉点在formMode
  3. 编辑走map更新原数组
  4. 新增走“往前插入新记录”

2. 为什么这里编辑用的是map

因为编辑的思路不是删除再新增,而是:

保留其他所有记录,只替换目标那一项。

这正适合map

十二、为什么编辑成功后也要切回新增模式

这一点非常重要。

很多人第一次实现编辑功能时,保存成功后会忘了处理页面状态。

这时候就会出现:

  • 表单里还是上一条数据
  • 按钮还显示“保存修改”
  • 用户下一次录入时不知道现在算新增还是继续改

所以保存成功后更稳的节奏通常是:

  1. 更新原列表
  2. 重置表单
  3. 退出编辑模式
  4. 给出新的提示文案

这也正是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一变,至少这些东西都可能跟着变:

  1. 列表内容
  2. 筛选结果
  3. 统计卡片
  4. 空状态判断
  5. 本地存储

这其实是在反复提醒你一件事:

页面真正的核心,不是某个按钮做了什么,而是原始状态变了以后,整个页面如何一起跟着更新。

十六、把编辑和删除真正接进RecordList

现在我们把列表组件继续往前推进。

这一版里,RecordList除了展示记录外,还要承担:

  1. 把“编辑”动作抛给父组件
  2. 把“删除”动作抛给父组件

先看 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. 为什么onDeleteid就够了

因为删除动作当前只需要知道:

要删除哪一条

所以传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. 为什么这一步值得做

因为后面你至少会在两个地方用到它:

  1. 读取本地数据
  2. 保存本地数据

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.stringifyJSON.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

而这些内容:

  • filteredRecordList
  • summaryData
  • 空状态文案

本来就可以根据原始数据重新算出来。

1. 这样做有什么好处

  1. 存储结构更简单
  2. 恢复页面时更清楚
  3. 不容易出现多份结果不同步

2. 这也再次印证了什么

这再次印证了上一章最关键的一条思路:

原始状态和派生结果要尽量分开理解。

二十六、如果当前正在编辑某条记录,删除它时为什么要特别小心

这一点看起来像边角问题,但在真实项目里很重要。

想象这样一个场景:

  1. 你点开了某条记录准备编辑
  2. 表单已经回填了这条记录内容
  3. 然后你又把这条记录删掉了

这时候如果页面还继续停留在旧的编辑状态,就会很怪:

  • 表单里还显示已不存在的数据
  • 提交按钮还写着“保存修改”
  • 当前editingRecordId也已经失效

所以更稳的处理一定是:

只要删掉的是当前正在编辑的记录,就立刻切回新增模式。

这就是为什么我们前面在handleDeleteRecord里专门加了那段判断。

二十七、现在把这一章的App主体真正串起来

到这里,我们已经有了这些核心部分:

  • recordList
  • formValue
  • message
  • filterType
  • formMode
  • editingRecordId
  • filteredRecordList
  • summaryData

所以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. 新增时发生了什么

发生的是:

  1. 用户输入表单
  2. 表单值写进formValue
  3. 提交后校验
  4. 校验通过后生成新记录
  5. 新记录进入recordList
  6. 列表、统计、筛选结果和本地存储一起联动更新

2. 编辑时发生了什么

发生的是:

  1. 用户点击列表项的“编辑”
  2. 页面进入编辑模式
  3. 目标记录内容回填进表单
  4. 提交后更新原数组中的那一项
  5. 页面切回新增模式
  6. 列表、统计和本地存储一起同步

3. 删除时发生了什么

发生的是:

  1. 用户点击“删除”
  2. 原数组过滤掉目标记录
  3. 页面相关展示全部重新计算
  4. 如果删的是正在编辑的记录,表单也一起恢复
  5. 本地存储自动写入最新结果

4. 这一章最关键的一句话是什么

就是:

新增、编辑、删除看起来是不同功能,但它们最终都在更新同一份原始记录状态,而页面的其他部分会围绕这份状态一起联动。

二十九、这一章最容易踩的几个坑

这一节建议你认真看。

因为项目一旦进入“编辑 + 删除 + 持久化”,细节问题会明显变多。

1. 坑一:编辑时直接改原对象

这会让数据变化边界变得很模糊。

当前阶段更稳的做法仍然是:

通过状态更新返回新结果,而不是直接在原对象上改字段。

2. 坑二:忘记区分新增模式和编辑模式

这样用户点“保存修改”时,你可能会错误地新增出一条重复记录。

3. 坑三:编辑成功后忘记切回新增模式

这样页面会一直停留在旧的编辑状态里。

4. 坑四:删除当前编辑项后,没有清理编辑状态

这会让页面进入一种很别扭、也很容易出错的状态。

5. 坑五:本地存储里什么都想保存

当前阶段先保存原始记录就够了。

不要急着把派生结果也一起塞进去。

6. 坑六:读取本地数据时不做异常保护

一旦 JSON 解析失败,页面就可能直接报错。

7. 坑七:新增、编辑、删除之后,只想着改列表,不去想统计和存储

一定要记住:

只要原始列表状态变了,页面的其他依赖结果通常也会跟着一起变。

三十、本章实践练习

这一章的练习重点,是把“模式切换、列表更新和持久化保存”真正练熟。

1. 练习 1:给表单补上“取消编辑”按钮

请你在RecordForm里补出:

  1. 编辑模式才显示的“取消编辑”按钮
  2. 点击后切回新增模式

这个练习会帮助你真正理解:

编辑模式不只是能进入,也必须能稳定退出。

2. 练习 2:让删除按钮先弹出确认提示

你可以尝试在删除前先做一个简单确认:

window.confirm("确定要删除这条账单记录吗?")

这个练习会帮助你开始思考:

有些交互不仅是“能做”,还要考虑用户是不是容易误操作。

3. 练习 3:手动刷新页面,验证本地存储是否真的生效

请你依次测试:

  1. 新增一条记录后刷新
  2. 编辑一条记录后刷新
  3. 删除一条记录后刷新

这个练习的重点是:

真正确认“原始状态变化 -> 本地保存 -> 刷新恢复”这条链路已经跑顺。

4. 练习 4:给本地存储增加版本前缀或更清楚的键名

例如尝试调整成:

  • from-zero-frontend-account-board-v1

这个练习会帮助你建立:

存储键名也属于项目设计的一部分。

三十一、学习重点提示

这一章请你重点记住下面这些话:

  1. 编辑、删除和本地存储虽然表面不同,但本质上都在围绕同一份原始记录状态工作。
  2. 新增和编辑通常共用同一个表单,不同的是提交后的处理逻辑。
  3. formModeeditingRecordId是编辑能力里非常关键的两条状态。
  4. 删除一条记录,本质上通常是在生成一份不包含它的新数组。
  5. 把数据写进localStorage属于副作用,所以很适合放在useEffect里。
  6. 本地存储优先保存原始记录数据,而不是筛选结果和统计结果。
  7. 只要原始列表状态变化,列表、统计、筛选结果和本地存储都会跟着联动。
  8. 退出编辑模式同样是一个值得单独组织的页面行为。
  9. 项目越往后做,越要学会区分“原始状态”“派生结果”和“副作用”。

如果你只记一句话,请记住:

这一章真正要建立的,不只是“会写编辑和删除”,而是“会让同一份原始数据在页面、模式和本地存储之间稳定流动起来”。

三十二、本章小结

这一章,我们正式把记账板从“可录入、可筛选、可统计”推进到了“更接近真实可用状态”的阶段。

你已经理解了:

  • 为什么新增和编辑通常共用同一个表单
  • 为什么编辑模式需要formModeeditingRecordId
  • 如何让某条记录回填进表单并进入编辑状态
  • 删除交互为什么本质上仍然是在更新原数组
  • 为什么把数据写入本地存储属于副作用
  • 如何通过useState初始化函数恢复数据
  • 如何通过useEffect自动保存最新列表
  • 为什么只保存原始记录数据通常会更稳

更重要的是,你开始真正建立一种很关键的项目感:

一个 React 小项目真正开始变得像“工具”,往往不是因为页面更花,而是因为数据能被修改、能被删除、能被保留下来。

这一步非常关键。

因为从这里开始,你已经不只是在给项目加功能,而是在真正把它推进到:

更接近真实使用场景的可持续状态。

三十三、课后思考题

请你认真思考下面这些问题:

  1. 为什么说编辑、删除和本地存储都在围绕同一份原始记录状态工作?
  2. 为什么新增和编辑通常共用一个表单,而不是直接做两套输入区?
  3. formModeeditingRecordId分别在解决什么问题?
  4. 为什么删除当前正在编辑的记录时,页面需要额外清理编辑状态?
  5. 为什么把recordList写到localStorage很适合放进useEffect
  6. 为什么本地存储通常先只保存原始记录,而不是把筛选结果和统计数字也一起保存?
  7. 为什么说新增、编辑、删除虽然功能不同,但最后都会推动整个页面一起联动?

建议你把这些问题用自己的话写下来。

只要你能把这些问题讲清楚,说明你已经真正开始进入 React 综合项目完整交互的主线了。

三十四、下一篇预告

接下来,我们会正式结束 React 阶段综合实战,并准备进入第五阶段的内容。

下一章我们会进入:

从零开始学前端 | 第三十七章:为什么学习 Next.js

到那时,你会开始理解这些问题:

  • Next.js 和 React 到底是什么关系
  • 为什么只会 React 还不够
  • 路由、工程能力和服务端能力为什么会变得重要

也就是说,下一章开始,我们会从“React 小项目综合实战”继续走到:

现代 React 应用框架的下一步认知。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询