使用状态响应输入
React 提供了一种声明式的方式来操作 UI。你不是直接操作 UI 的各个部分,而是描述组件可能处于的不同状态,并根据用户输入在这些状态之间切换。这与设计师思考 UI 的方式类似。
您将学习
- 声明式 UI 编程与命令式 UI 编程有何不同
- 如何枚举组件可能处于的不同视觉状态
- 如何从代码中触发不同视觉状态之间的变化
声明式 UI 与命令式 UI 的比较
当你设计 UI 交互时,你可能会思考 UI 如何响应用户操作而变化。考虑一个允许用户提交答案的表单:
- 当你在表单中输入内容时,“提交”按钮变为启用状态。
- 当你按下“提交”时,表单和按钮都变为禁用状态,并且一个加载指示器出现。
- 如果网络请求成功,表单被隐藏,并且“感谢”消息出现。
- 如果网络请求失败,一条错误消息出现,并且表单再次变为启用状态。
在命令式编程中,上述过程直接对应于你实现交互的方式。你必须根据刚刚发生的情况编写确切的指令来操作 UI。换一种方式来思考:想象你坐在副驾驶,一步一步地告诉司机在哪里转弯。

插图作者:Rachel Lee Nabors
司机不知道你想去哪里,他们只是遵循你的命令。(如果你指错了方向,最终就会到达错误的地方!)这被称为命令式,因为你必须“命令”每个元素,从加载指示器到按钮,告诉计算机如何更新 UI。
在这个命令式 UI 编程的例子中,表单是未使用React 构建的。它只使用了浏览器的DOM:
对于孤立的示例,命令式地操作 UI 效果尚可,但在更复杂的系统中,管理难度会呈指数级增长。想象一下更新一个充满此类不同表单的页面。添加一个新的 UI 元素或新的交互将需要仔细检查所有现有代码,以确保你没有引入错误(例如,忘记显示或隐藏某些内容)。
React 就是为了解决这个问题而构建的。
在 React 中,你不直接操作 UI——也就是说,你不直接启用、禁用、显示或隐藏组件。相反,你声明你想要显示什么,然后由 React 来计算出如何更新 UI。想象一下,你上了一辆出租车,告诉司机你想去哪里,而不是确切地告诉他们该在哪里转弯。司机的职责就是把你送到那里,他们甚至可能知道一些你未曾考虑过的捷径!

插图作者:Rachel Lee Nabors
以声明式方式思考 UI
你已经看到了上面如何以命令式的方式实现一个表单。为了更好地理解如何在 React 中思考,你将在下面逐步重新在 React 中实现这个 UI:
- 识别组件的不同视觉状态
- 确定触发这些状态变化的原因
- 使用
useState在内存中表示状态 - 移除任何非必要的状态变量
- 连接事件处理函数以设置状态
步骤 1:识别组件的不同视觉状态
在计算机科学中,你可能听说过“状态机”处于若干“状态”之一。如果你与设计师合作,你可能见过不同“视觉状态”的模型。React 处于设计与计算机科学的交叉点,因此这两种思想都是灵感的来源。
你需要可视化用户可能看到的所有不同的 UI“状态”:
- 空:表单有一个禁用的“提交”按钮。
- 输入中:表单有一个启用的“提交”按钮。
- 提交中:表单完全禁用。显示加载指示器。
- 成功:显示“感谢”消息而非表单。
- 错误:与“输入中”状态相同,但多了一条错误消息。
就像设计师一样,在添加逻辑之前,你会想要为不同的状态“制作模型”或“创建模型”。例如,这里有一个仅针对表单视觉部分的模型。这个模型由一个名为status的 prop 控制,其默认值为'empty':
你可以随意命名这个 prop,名称并不重要。尝试将 status = 'empty'编辑为status = 'success' 以查看成功消息出现。模型化让你可以在连接任何逻辑之前快速迭代 UI。这里是同一个组件更完善的模型,仍然由 statusprop“控制”:
步骤 2:确定触发这些状态变化的原因
你可以响应两种输入来触发状态更新:
- 人为输入,例如点击按钮、在字段中键入、导航链接。
- 计算机输入,例如网络响应到达、超时完成、图像加载。


插图作者:Rachel Lee Nabors
在这两种情况下,你必须设置状态变量来更新 UI。对于你正在开发的表单,你需要响应几种不同的输入来改变状态:
- 更改文本输入(人为操作)应使其从 空状态切换到输入中状态或切换回来,具体取决于文本框是否为空。
- 点击提交按钮(人为操作)应使其切换到提交中状态。
- 成功的网络响应(计算机操作)应使其切换到成功状态。
- 失败的网络响应(计算机操作)应使其切换到错误状态,并显示匹配的错误信息。
注意
请注意,人为输入通常需要事件处理函数!
为了帮助可视化这个流程,可以尝试在纸上将每个状态画成一个带标签的圆圈,将每两个状态之间的变化画成一个箭头。通过这种方式,你可以在实现之前勾勒出许多流程并找出错误。


表单状态
步骤 3:使用 useState 在内存中表示状态
接下来,你需要使用 useState在内存中表示组件的视觉状态。关键在于简单:每个状态都是一个“活动部件”,你希望尽可能减少“活动部件”的数量。复杂度越高,错误越多!
从那些绝对必须存在的状态开始。例如,你需要存储输入的answer,以及存储最后一个错误的error(如果存在的话):
然后,你需要一个状态变量来表示你想要显示的视觉状态。在内存中表示这一点通常不止一种方法,因此你需要进行实验。
如果你一时想不出最佳方法,可以先添加足够的状态,确保覆盖所有可能的视觉状态:
你的第一个想法可能不是最好的,但这没关系——重构状态是这个过程的一部分!
步骤 4:移除任何非必需的状态变量
你需要避免状态内容重复,只跟踪必要的内容。花一点时间重构你的状态结构将使你的组件更容易理解,减少重复,并避免意外的含义。你的目标是防止内存中的状态不代表任何你希望用户看到的有效用户界面的情况。(例如,你永远不希望同时显示错误消息并禁用输入,否则用户将无法纠正错误!)
你可以对你的状态变量提出以下问题:
- 这个状态会导致悖论吗?例如,
isTyping和isSubmitting不可能同时为true。悖论通常意味着状态约束不够。两个布尔值有四种可能的组合,但只有三种对应有效状态。为了移除“不可能”的状态,你可以将它们合并为一个status,它必须是三个值之一:'typing'、'submitting'或'success'。 - 相同的信息是否已存在于另一个状态变量中?另一个悖论:
isEmpty和isTyping不可能同时为true。将它们设为独立的状态变量,可能导致它们不同步并引发错误。幸运的是,你可以移除isEmpty,转而检查answer.length === 0。 - 你能从另一个状态变量的反状态中获取相同的信息吗?
isError是不必要的,因为你可以检查error !== null来代替。
经过这次清理,你只剩下3个(从7个减少而来!)必需的状态变量:
你知道它们是必需的,因为移除任何一个都会破坏功能。
步骤 5:将事件处理程序连接到状态设置
创建更新状态的事件处理程序。下面是最终的表单,所有事件处理程序都已连接:
虽然这段代码比最初的命令式示例要长,但它更加健壮。将所有交互表达为状态变化,让你以后可以引入新的视觉状态而不会破坏现有的状态。它还允许你改变每个状态下应该显示的内容,而无需更改交互逻辑本身。
回顾
- 声明式编程意味着为每个视觉状态描述 UI,而不是微观管理 UI(命令式)。
- 开发组件时:
- 识别其所有视觉状态。
- 确定状态变化的人为和计算机触发因素。
- 使用
useState对状态进行建模。 - 移除非必需状态以避免错误和悖论。
- 将事件处理程序连接到状态设置。
Try out some challenges
Challenge 1 of 3:Add and remove a CSS class #
Make it so that clicking on the picture removes the background--active CSS class from the outer <div>, but adds the picture--active class to the <img>. Clicking the background again should restore the original CSS classes.
Visually, you should expect that clicking on the picture removes the purple background and highlights the picture border. Clicking outside the picture highlights the background, but removes the picture border highlight.
