React provides a declarative way to manipulate the UI. Instead of manipulating individual pieces of the UI directly, you describe the different states that your component can be in, and switch between them in response to the user input. This is similar to how designers think about the UI. React 控制 UI 的方式是声明式的。你不必直接控制 UI 的各个部分,只需要声明组件可以处于的不同状态,并根据用户的输入在它们之间切换。这与设计师对 UI 的思考方式很相似。
你将会学习到
- How declarative UI programming differs from imperative UI programming
- 了解声明式 UI 编程与命令式 UI 编程有何不同
- How to enumerate the different visual states your component can be in
- 了解如何列举组件可能处于的不同视图状态
- How to trigger the changes between the different visual states from code
- 了解如何在代码中触发不同视图状态的变化
声明式 UI 与命令式 UI 的比较 | How declarative UI compares to imperative
When you design UI interactions, you probably think about how the UI changes in response to user actions. Consider a form that lets the user submit an answer: 当你设计 UI 交互时,可能会去思考 UI 如何根据用户的操作而响应变化。想象一个让用户提交答案的表单:
- When you type something into the form, the “Submit” button becomes enabled.
- 当你向表单输入数据时,“提交”按钮会随之变成可用状态
- When you press “Submit”, both the form and the button become disabled, and a spinner appears.
- 当你点击“提交”后,表单和提交按钮都会随之变成不可用状态,并且会加载动画会随之出现
- If the network request succeeds, the form gets hidden, and the “Thank you” message appears.
- 如果网络请求成功,表单会随之隐藏,同时“提交成功”的信息会随之出现
- If the network request fails, an error message appears, and the form becomes enabled again.
- 如果网络请求失败,错误信息会随之出现,同时表单又变为可用状态
In imperative programming, the above corresponds directly to how you implement interaction. You have to write the exact instructions to manipulate the UI depending on what just happened. Here’s another way to think about this: imagine riding next to someone in a car and telling them turn by turn where to go. 在 命令式编程 中,以上的过程直接告诉你如何去实现交互。你必须去根据要发生的事情写一些明确的命令去操作 UI。对此有另一种理解方式,想象一下,当你坐在车里的某个人旁边,然后一步一步地告诉他该去哪。
They don’t know where you want to go, they just follow your commands. (And if you get the directions wrong, you end up in the wrong place!) It’s called imperative because you have to “command” each element, from the spinner to the button, telling the computer how to update the UI. 他并不知道你想去哪,只想跟着命令行动。(并且如果你发出了错误的命令,那么你就会到达错误的地方)正因为你必须从加载动画到按钮地“命令”每个元素,所以这种告诉计算机如何去更新 UI 的编程方式被称为命令式编程
In this example of imperative UI programming, the form is built without React. It only uses the browser DOM: 在这个命令式 UI 编程的例子中,表单没有使用 React 生成,而是使用原生的 DOM:
async function handleFormSubmit(e) { e.preventDefault(); disable(textarea); disable(button); show(loadingMessage); hide(errorMessage); try { await submitForm(textarea.value); show(successMessage); hide(form); } catch (err) { show(errorMessage); errorMessage.textContent = err.message; } finally { hide(loadingMessage); enable(textarea); enable(button); } } function handleTextareaChange() { if (textarea.value.length === 0) { disable(button); } else { enable(button); } } function hide(el) { el.style.display = 'none'; } function show(el) { el.style.display = ''; } function enable(el) { el.disabled = false; } function disable(el) { el.disabled = true; } function submitForm(answer) { // Pretend it's hitting the network. return new Promise((resolve, reject) => { setTimeout(() => { if (answer.toLowerCase() === 'istanbul') { resolve(); } else { reject(new Error('Good guess but a wrong answer. Try again!')); } }, 1500); }); } let form = document.getElementById('form'); let textarea = document.getElementById('textarea'); let button = document.getElementById('button'); let loadingMessage = document.getElementById('loading'); let errorMessage = document.getElementById('error'); let successMessage = document.getElementById('success'); form.onsubmit = handleFormSubmit; textarea.oninput = handleTextareaChange;
Manipulating the UI imperatively works well enough for isolated examples, but it gets exponentially more difficult to manage in more complex systems. Imagine updating a page full of different forms like this one. Adding a new UI element or a new interaction would require carefully checking all existing code to make sure you haven’t introduced a bug (for example, forgetting to show or hide something). 对于独立系统来说,命令式地控制用户界面的效果也不错,但是当处于更加复杂的系统中时,这会造成管理的困难程度指数级地增长。如同示例一样,想象一下,当你想更新这样一个包含着不同表单的页面时,你想要添加一个新 UI 元素或一个新的交互,为了保证不会因此产生新的 bug(例如忘记去显示或隐藏一些东西),你必须十分小心地去检查所有已经写好的代码。
React was built to solve this problem. React 正是为了解决这样的问题而诞生的。
In React, you don’t directly manipulate the UI—meaning you don’t enable, disable, show, or hide components directly. Instead, you declare what you want to show, and React figures out how to update the UI. Think of getting into a taxi and telling the driver where you want to go instead of telling them exactly where to turn. It’s the driver’s job to get you there, and they might even know some shortcuts you haven’t considered! 在 React 中,你不必直接去操作 UI —— 你不必直接启用、关闭、显示或隐藏组件。相反,你只需要 声明你想要显示的内容, React 就会通过计算得出该如何去更新 UI。想象一下,当你上了一辆出租车并且告诉司机你想去哪,而不是事无巨细地告诉他该如何走。将你带到目的地是司机的工作,他们甚至可能知道一些你没有想过并且不知道的捷径!
声明式地考虑 UI | Thinking about UI declaratively
You’ve seen how to implement a form imperatively above. To better understand how to think in React, you’ll walk through reimplementing this UI in React below: 你已经从上面的例子看到如何去实现一个表单了,为了更好地理解如何在 React 中思考,接下来你将会学到如何用 React 重新实现这个 UI:
- Identify your component’s different visual states
- 定位你的组件中不同的视图状态
- Determine what triggers those state changes
- 确定是什么触发了这些 state 的改变
- Represent the state in memory using
useState
- 表示内存中的 state(需要使用
useState
) - Remove any non-essential state variables
- 删除任何不必要的 state 变量
- Connect the event handlers to set the state
- 连接事件处理函数去设置 state
步骤 1:定位组件中不同的视图状态 | Step 1: Identify your component’s different visual states
In computer science, you may hear about a “state machine” being in one of several “states”. If you work with a designer, you may have seen mockups for different “visual states”. React stands at the intersection of design and computer science, so both of these ideas are sources of inspiration. 在计算机科学中,你或许听过可处于多种“状态”之一的 “状态机”。如果你有与设计师一起工作,那么你可能已经见过不同“视图状态”的模拟图。正因为 React 站在设计与计算机科学的交点上,因此这两种思想都是灵感的来源。
First, you need to visualize all the different “states” of the UI the user might see: 首先,你需要去可视化 UI 界面中用户可能看到的所有不同的“状态”:
- Empty: Form has a disabled “Submit” button.
- 无数据:表单有一个不可用状态的“提交”按钮。
- Typing: Form has an enabled “Submit” button.
- 输入中:表单有一个可用状态的“提交”按钮。
- Submitting: Form is completely disabled. Spinner is shown.
- 提交中:表单完全处于不可用状态,加载动画出现。
- Success: “Thank you” message is shown instead of a form.
- 成功时:显示“成功”的消息而非表单。
- Error: Same as Typing state, but with an extra error message.
- 错误时:与输入状态类似,但会多错误的消息。
Just like a designer, you’ll want to “mock up” or create “mocks” for the different states before you add logic. For example, here is a mock for just the visual part of the form. This mock is controlled by a prop called status
with a default value of 'empty'
:
像一个设计师一样,你会想要在你添加逻辑之前去“模拟”不同的状态或创建“模拟状态”。例如下面的例子,这是一个对表单可视部分的模拟。这个模拟被一个 status
的属性控制,并且这个属性的默认值为 empty
。
export default function Form({ status = 'empty' }) { if (status === 'success') { return <h1>That's right!</h1> } return ( <> <h2>City quiz</h2> <p> In which city is there a billboard that turns air into drinkable water? </p> <form> <textarea /> <br /> <button> Submit </button> </form> </> ) }
You could call that prop anything you like, the naming is not important. Try editing status = 'empty'
to status = 'success'
to see the success message appear. Mocking lets you quickly iterate on the UI before you wire up any logic. Here is a more fleshed out prototype of the same component, still “controlled” by the status
prop:
你可以随意命名这个属性,名字并不重要。试着将 status = 'empty'
改为 status = 'success'
,然后你就会看到成功的信息出现。模拟可以让你在书写逻辑前快速迭代 UI。这是同一组件的一个更加充实的原型,仍然由 status
属性“控制”:
export default function Form({ // Try 'submitting', 'error', 'success': status = 'empty' }) { if (status === 'success') { return <h1>That's right!</h1> } return ( <> <h2>City quiz</h2> <p> In which city is there a billboard that turns air into drinkable water? </p> <form> <textarea disabled={ status === 'submitting' } /> <br /> <button disabled={ status === 'empty' || status === 'submitting' }> Submit </button> {status === 'error' && <p className="Error"> Good guess but a wrong answer. Try again! </p> } </form> </> ); }
深入探讨
If a component has a lot of visual states, it can be convenient to show them all on one page: 如果一个组件有多个视图状态,你可以很方便地将它们展示在一个页面中:
import Form from './Form.js'; let statuses = [ 'empty', 'typing', 'submitting', 'success', 'error', ]; export default function App() { return ( <> {statuses.map(status => ( <section key={status}> <h4>Form ({status}):</h4> <Form status={status} /> </section> ))} </> ); }
Pages like this are often called “living styleguides” or “storybooks”. 类似这样的页面通常被称作“living styleguide”或“storybook”。
步骤 2:确定是什么触发了这些状态的改变 | Step 2: Determine what triggers those state changes
You can trigger state updates in response to two kinds of inputs: 你可以触发 state 的更新来响应两种输入:
- Human inputs, like clicking a button, typing in a field, navigating a link.
- 人为输入。比如点击按钮、在表单中输入内容,或导航到链接。
- Computer inputs, like a network response arriving, a timeout completing, an image loading.
- 计算机输入。比如网络请求得到反馈、定时器被触发,或加载一张图片。


In both cases, you must set state variables to update the UI. For the form you’re developing, you will need to change state in response to a few different inputs: 以上两种情况中,你必须设置 state 变量 去更新 UI。对于正在开发中的表单来说,你需要改变 state 以响应几个不同的输入:
- Changing the text input (human) should switch it from the Empty state to the Typing state or back, depending on whether the text box is empty or not.
- 改变输入框中的文本时(人为)应该根据输入框的内容是否是空值,从而决定将表单的状态从空值状态切换到输入中或切换回原状态。
- Clicking the Submit button (human) should switch it to the Submitting state.
- 点击提交按钮时(人为)应该将表单的状态切换到提交中的状态。
- Successful network response (computer) should switch it to the Success state.
- 网络请求成功后(计算机)应该将表单的状态切换到成功的状态。
- Failed network response (computer) should switch it to the Error state with the matching error message.
- 网络请求失败后(计算机)应该将表单的状态切换到失败的状态,与此同时,显示错误信息。
To help visualize this flow, try drawing each state on paper as a labeled circle, and each change between two states as an arrow. You can sketch out many flows this way and sort out bugs long before implementation. 为了可视化这个流程,请尝试在纸上画出圆形标签以表示每个状态,两个状态之间的改变用箭头表示。你可以像这样画出很多流程并且在写代码前解决许多 bug。


Form states 表单的各种状态
步骤 3:通过 useState
表示内存中的 state | Step 3: Represent the state in memory with useState
Next you’ll need to represent the visual states of your component in memory with useState
. Simplicity is key: each piece of state is a “moving piece”, and you want as few “moving pieces” as possible. More complexity leads to more bugs!
接下来你会需要在内存中通过 useState
表示组件中的视图状态。诀窍很简单:state 的每个部分都是“处于变化中的”,并且你需要让“变化的部分”尽可能的少。更复杂的程序会产生更多 bug!
Start with the state that absolutely must be there. For example, you’ll need to store the answer
for the input, and the error
(if it exists) to store the last error:
先从绝对必须存在的状态开始。例如,你需要存储输入的 answer
以及用于存储最后一个错误的 error
(如果存在的话):
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
Then, you’ll need a state variable representing which one of the visual states that you want to display. There’s usually more than a single way to represent that in memory, so you’ll need to experiment with it. 接下来,你需要一个状态变量来代表你想要显示的那个可视状态。通常有多种方式在内存中表示它,因此你需要进行实验。
If you struggle to think of the best way immediately, start by adding enough state that you’re definitely sure that all the possible visual states are covered: 如果你很难立即想出最好的办法,那就先从添加足够多的 state 开始,确保所有可能的视图状态都囊括其中:
const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);
Your first idea likely won’t be the best, but that’s ok—refactoring state is a part of the process! 你最初的想法或许不是最好的,但是没关系,重构 state 也是步骤中的一部分!
步骤 4:删除任何不必要的 state 变量 | Step 4: Remove any non-essential state variables
You want to avoid duplication in the state content so you’re only tracking what is essential. Spending a little time on refactoring your state structure will make your components easier to understand, reduce duplication, and avoid unintended meanings. Your goal is to prevent the cases where the state in memory doesn’t represent any valid UI that you’d want a user to see. (For example, you never want to show an error message and disable the input at the same time, or the user won’t be able to correct the error!) 你会想要避免 state 内容中的重复,从而只需要关注那些必要的部分。花一点时间来重构你的 state 结构,会让你的组件更容易被理解,减少重复并且避免歧义。你的目的是防止出现在内存中的 state 不代表任何你希望用户看到的有效 UI 的情况。(比如你绝对不会想要在展示错误信息的同时禁用掉输入框,导致用户无法纠正错误!)
Here are some questions you can ask about your state variables: 这有一些你可以问自己的, 关于 state 变量的问题:
- Does this state cause a paradox? For example,
isTyping
andisSubmitting
can’t both betrue
. A paradox usually means that the state is not constrained enough. There are four possible combinations of two booleans, but only three correspond to valid states. To remove the “impossible” state, you can combine these into astatus
that must be one of three values:'typing'
,'submitting'
, or'success'
. - 这个 state 是否会导致矛盾?例如,
isTyping
与isSubmitting
的状态不能同时为true
。矛盾的产生通常说明了这个 state 没有足够的约束条件。两个布尔值有四种可能的组合,但是只有三种对应有效的状态。为了将“不可能”的状态移除,你可以将他们合并到一个'status'
中,它的值必须是'typing'
、'submitting'
以及'success'
这三个中的一个。 - Is the same information available in another state variable already? Another paradox:
isEmpty
andisTyping
can’t betrue
at the same time. By making them separate state variables, you risk them going out of sync and causing bugs. Fortunately, you can removeisEmpty
and instead checkanswer.length === 0
. - 相同的信息是否已经在另一个 state 变量中存在?另一个矛盾:
isEmpty
和isTyping
不能同时为true
。通过使它们成为独立的 state 变量,可能会导致它们不同步并导致 bug。幸运的是,你可以移除isEmpty
转而用message.length === 0
。 - Can you get the same information from the inverse of another state variable?
isError
is not needed because you can checkerror !== null
instead. - 你是否可以通过另一个 state 变量的相反值得到相同的信息?
isError
是多余的,因为你可以检查error !== null
。
After this clean-up, you’re left with 3 (down from 7!) essential state variables: 在清理之后,你只剩下 3 个(从原本的 7 个!)必要的 state 变量:
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'
You know they are essential, because you can’t remove any of them without breaking the functionality. 正是因为你不能在不破坏功能的情况下删除其中任何一个状态变量,因此你可以确定这些都是必要的。
深入探讨
These three variables are a good enough representation of this form’s state. However, there are still some intermediate states that don’t fully make sense. For example, a non-null error
doesn’t make sense when status
is 'success'
. To model the state more precisely, you can extract it into a reducer. Reducers let you unify multiple state variables into a single object and consolidate all the related logic!
尽管这三个变量对于表示这个表单的状态来说已经足够好了,仍然是有一些中间状态并不是完全有意义的。例如一个非空的 error
当 status
的值为 success
时没有意义。为了更精确地模块化状态,你可以 将状态提取到一个 reducer 中。Reducer 可以让您合并多个状态变量到一个对象中并巩固所有相关的逻辑!
步骤 5:连接事件处理函数以设置 state | Step 5: Connect the event handlers to set state
Lastly, create event handlers that update the state. Below is the final form, with all event handlers wired up: 最后,创建事件处理函数去设置 state 变量。下面是绑定好事件的最终表单:
import { useState } from 'react'; export default function Form() { const [answer, setAnswer] = useState(''); const [error, setError] = useState(null); const [status, setStatus] = useState('typing'); if (status === 'success') { return <h1>That's right!</h1> } async function handleSubmit(e) { e.preventDefault(); setStatus('submitting'); try { await submitForm(answer); setStatus('success'); } catch (err) { setStatus('typing'); setError(err); } } function handleTextareaChange(e) { setAnswer(e.target.value); } return ( <> <h2>City quiz</h2> <p> In which city is there a billboard that turns air into drinkable water? </p> <form onSubmit={handleSubmit}> <textarea value={answer} onChange={handleTextareaChange} disabled={status === 'submitting'} /> <br /> <button disabled={ answer.length === 0 || status === 'submitting' }> Submit </button> {error !== null && <p className="Error"> {error.message} </p> } </form> </> ); } function submitForm(answer) { // Pretend it's hitting the network. return new Promise((resolve, reject) => { setTimeout(() => { let shouldError = answer.toLowerCase() !== 'lima' if (shouldError) { reject(new Error('Good guess but a wrong answer. Try again!')); } else { resolve(); } }, 1500); }); }
Although this code is longer than the original imperative example, it is much less fragile. Expressing all interactions as state changes lets you later introduce new visual states without breaking existing ones. It also lets you change what should be displayed in each state without changing the logic of the interaction itself. 尽管这些代码相对与最初的命令式的例子来说更长,但是却更加健壮。将所有的交互变为 state 的改变,可以让你避免之后引入新的视图状态后导致现有 state 被破坏。同时也使你在不必改变交互逻辑的情况下,更改每个状态对应的 UI。
摘要
- Declarative programming means describing the UI for each visual state rather than micromanaging the UI (imperative).
- 声明式编程意味着为每个视图状态声明 UI 而非细致地控制 UI(命令式)。
- When developing a component:
- 当开发一个组件时:
- Identify all its visual states.
- 写出你的组件中所有的视图状态。
- Determine the human and computer triggers for state changes.
- 确定是什么触发了这些 state 的改变。
- Model the state with
useState
.
- 通过
useState
模块化内存中的 state。
- Remove non-essential state to avoid bugs and paradoxes.
- 删除任何不必要的 state 变量。
- Connect the event handlers to set state.
- 连接事件处理函数去设置 state。
第 1 个挑战 共 3 个挑战: 添加和删除一个 CSS class | 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.
尝试实现当点击图片时删除外部 <div>
的 CSS class background--active
,并将 picture--active
的 CSS class 添加到 <img>
上。当再次点击背景图片时将恢复最开始的 CSS class。
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. 视觉上,你应该期望当点击图片时会移除紫色的背景,并且高亮图片的边框。点击图片外面时高亮背景并且删除图片边框的高亮效果。
export default function Picture() { return ( <div className="background background--active"> <img className="picture" alt="Rainbow houses in Kampung Pelangi, Indonesia" src="https://i.imgur.com/5qwVYb1.jpeg" /> </div> ); }