LESSON 4.4 / MODERN FRONTEND

数据、状态与 URL 驱动界面

现代界面不是不断手动操作页面,而是让页面根据数据和状态长成应该有的样子。

预计阅读15–20 分钟
完成结果能从状态变化解释一个交互为什么更新界面。
本课目标
  • 能从状态变化解释一个交互为什么更新界面。
  • 内容数据与界面结构分离
  • 制作可筛选列表,让筛选条件进入 URL,并处理加载、空结果和错误状态。
开始之前
  • 完成 4.3「React:用组件组织界面」
01

FOUNDATION

必须理解

现代界面不是不断手动操作页面,而是让页面根据数据和状态长成应该有的样子。

状态(State)是会随交互变化并影响界面的数据。React 根据当前状态重新计算界面;如果同一事实被重复保存,多个副本就可能互相矛盾。

先建立整体直觉

餐厅叫号屏不需要另外保存“还有几桌”等每个结果,只要有排队名单就能计算。保存原始事实,其他显示值尽量现算。

贯穿本课的实际场景

任务数组是核心状态;当前筛选条件可以进入 URL;“已完成数量”由任务数组计算,不再单独维护一个容易失真的数字。

概念 1

内容数据与界面结构分离

先用白话理解

任务标题和完成状态属于内容数据;当前选中了哪个筛选、输入框写到哪里,则更像界面状态。先弄清一个值该由服务器、URL、页面还是小组件负责。

基础概念

内容数据描述产品中的事实,界面结构描述这些事实怎样呈现。把二者分离后,同一份任务数组可以生成列表、统计和筛选结果,而无需复制三份标记。

进一步理解

数据通常拥有稳定字段,例如 id、title、done;组件通过映射和条件渲染把它变成元素。改变数据会改变画面,改变布局不应反过来偷偷改业务事实。

每条可变记录需要稳定标识,React 才能在列表更新时追踪对应项。数组索引在排序或插入后会改变,不适合作为长期身份。

放进实际场景

任务数组作为唯一事实来源,列表逐条渲染,完成数量通过过滤计算,空数组则显示空状态。

容易混淆的地方

把 JSX 写进数据字段会把内容和表现绑死;把完成数量再次保存又会产生与任务数组不一致的第二份事实。

这一小节记住:保存最小事实,让界面和统计从事实计算出来。

概念 2

状态、事件与重新渲染

先用白话理解

点击事件会触发状态更新,React 再根据新状态画一次界面。别直接在原数组上改值,创建一份新数组更容易让 React 和你自己都看清变化。

基础概念

状态是组件需要在多次渲染之间记住、且变化后会影响界面的数据。事件触发状态更新,React 根据新状态再次计算组件树并更新必要的 DOM。

进一步理解

状态更新不是直接改当前变量,而是请求 React 安排下一次渲染。根据旧值计算新值时使用函数式更新,可以避免读取过期快照。

对象和数组应创建新值而不是原地修改,使变化可被可靠比较。共享状态放到最近共同父组件,避免多个组件各自维护不同版本。

放进实际场景

点击任务按钮后,setTasks 根据 id 生成新数组,只替换目标任务;React 重新渲染并更新对应文字。

容易混淆的地方

普通局部变量会在每次渲染重新计算,改变它也不会通知 React;状态则由 React 保存并触发新渲染。

这一小节记住:事件提出变化,状态保存事实,渲染把最新事实呈现出来。

概念 3

URL 作为可分享、可前进后退的页面状态

先用白话理解

像 ?filter=done 这样的 URL 可以把筛选状态保留下来,刷新、分享和前进后退都更自然。还没提交的输入文字通常只放组件里就够了。

基础概念

URL 可以保存适合分享、刷新和历史导航的页面状态,例如搜索词、筛选条件和页码。它是浏览器与用户都能看见的状态容器。

进一步理解

当页面结果由 URL 决定,复制地址即可重现视图,前进后退也能恢复选择。敏感信息、巨大对象和未提交输入不适合放进 URL。

组件本地状态适合短暂交互,服务器数据适合长期共享事实,URL 状态适合导航语义。先判断生命周期与共享范围,再选择位置。

放进实际场景

`/tasks?filter=done` 表示只看已完成任务;刷新或发给同伴后仍得到相同筛选,而输入框正在编辑的草稿留在本地状态。

容易混淆的地方

URL 参数不等于数据库数据,它可以描述当前视图,却不能替代服务端保存任务记录和权限判断。

这一小节记住:需要可分享、可刷新、可前进后退的视图选择,优先考虑 URL。

保存事实,计算结果
运行前先确认
  • 组件已从 React 导入 useState,并存在 Task 类型。
  • tasks 是当前页面中任务事实的唯一所有者。
const [tasks, setTasks] = useState<Task[]>([]);
const completedCount = tasks.filter((task) => task.done).length;

function toggleTask(id: string) {
  setTasks((current) => current.map((task) =>
    task.id === id ? { ...task, done: !task.done } : task
  ));
}
这段代码在做什么

完成数量由任务数组计算,切换时创建新数组,避免两个状态源互相不同步。

  1. useState 保存原始任务数组,completedCount 每次渲染都从数组计算。
  2. toggleTask 使用函数式更新取得最新数组,避免闭包中的旧值。
  3. map 创建新数组,仅为匹配 id 的任务创建新对象并反转 done。
你应该观察到

点击某条任务后只有该项状态变化,完成数量同步更新;原数组没有被直接修改,也不存在需要额外同步的 completedCount 状态。

02

AI COLLABORATION

AI 如何参与

让 AI 明确区分服务端数据、组件状态、URL 状态和临时输入,不要把所有东西都塞进 useState。

推荐协作顺序

  1. 1

    把页面中的所有可变值列给 AI,让它先分类来源。

  2. 2

    让它指出哪些值可以从其它数据计算,不要急着写 useState。

  3. 3

    实现后沿一次点击追踪状态变化,确认只有一个可信来源。

可直接使用的 Prompt
任务应用有 tasks、filter、completedCount、inputValue、isDialogOpen 五个值。请判断它们分别应来自服务端数据、URL 状态、可计算值或组件状态,并说明理由。再给出最小数据流图,不使用 useEffect 同步重复状态。
应该得到什么

每个值只有一个可信来源,completedCount 从 tasks 计算,filter 可由 URL 表达。

人工检查清单

  • 确认 AI 没把可计算值复制进 state,也没有用 effect 在多个状态之间来回同步。
  • 要求 AI 为每个值标出唯一所有者,并解释为何属于服务器、URL、页面或组件状态。
  • 通过刷新、分享链接、前进后退和快速连续点击验证状态没有重复或丢失。
03

COMMON TRAPS

常见误区

错误不是需要隐藏的失败,而是帮助你看清系统边界的证据。下面三类问题在 AI 辅助学习中最常出现。

误区 1

什么都放进 useState

你会看到
任务数组、完成数量和筛选后列表各保存一份,更新时经常不同步。
为什么发生
可计算值被重复保存后,需要每次同步更新;漏掉一次就会出现两个互相矛盾的真相。
怎样纠正
只保存原始事实,完成数量和可见列表在渲染时计算。
误区 2

直接修改原数组

你会看到
任务对象已经变了,React 却没有按预期刷新。
为什么发生
多个组件各自保存同一任务列表,局部更新不会自动传播,界面会显示不同版本。
怎样纠正
通过 map、filter 或展开语法创建新数组和新对象。
误区 3

刷新页面后筛选消失

你会看到
需要分享的筛选条件只存在组件内存。
为什么发生
副作用常被用来事后同步本可直接计算的数据,增加额外渲染与循环依赖。
怎样纠正
适合分享和前进后退的状态放进 URL 查询参数。
04

HANDS-ON

动手任务

制作可筛选列表,让筛选条件进入 URL,并处理加载、空结果和错误状态。

准备条件
  • 完成 4.3「React:用组件组织界面」

跟着做

  1. 01

    用 useState 保存本地任务数组和输入值。

  2. 02

    通过不可变更新实现新增与切换完成。

  3. 03

    用 filter 从 URL 或选择项决定显示列表。

  4. 04

    使用 filter 和 reduce 计算可见任务与完成数量。

  5. 05

    测试刷新、浏览器前进后退、空结果和重复点击。

完成标志

能从状态变化解释一个交互为什么更新界面。

加餐挑战

把筛选条件写入 URL 查询参数,并分享链接验证另一窗口打开相同筛选。

离开本课前,自问四件事

  • 内容数据与界面结构应如何分离?
  • 为什么 React 状态更新后会重新渲染,而普通变量不会?
  • 哪些信息适合放进 URL,哪些不适合?
  • 完成任务数量为什么应从任务数组计算而不是单独保存?
完成本课了吗?

确认完成后,会同步更新学习中心的课程学习进度。