使用状态对输入做出反应

React 提供了一种声明式的方式来操作 UI。你无需直接操作 UI 的各个部分,而是描述你的组件可能处于的不同状态,并在它们之间切换以响应用户输入。这类似于设计师对 UI 的看法。

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

你将学习到

  • 声明式 UI 编程与命令式 UI 编程有何不同

    ¥How declarative UI programming differs from imperative UI programming

  • 如何枚举组件可能处于的不同视觉状态

    ¥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

当你设计 UI 交互时,你可能会考虑 UI 如何响应用户操作而变化。考虑一个让用户提交答案的表单:

¥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:

  • 当你在表单中输入内容时,“提交” 按钮将变为启用状态。

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

在命令式编程中,上述内容直接对应于你如何实现交互。你必须根据刚刚发生的情况编写准确的指令来操作 UI。这是另一种思考方式:想象一下坐在车里的某人旁边并告诉他们轮流去哪里。

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

In a car driven by an anxious-looking person representing JavaScript, a passenger orders the driver to execute a sequence of complicated turn by turn navigations.

Illustrated by Rachel Lee Nabors

他们不知道你要去哪里,他们只是听从你的命令。(如果方向错误,你就会走错地方!)之所以称为命令式,是因为你必须对从加载控件到按钮的每个元素进行 “命令”,告诉计算机如何更新 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 编程示例中,表单是在没有 React 的情况下构建的。它只使用浏览器 DOM

¥In this example of imperative UI programming, the form is built without React. It only uses the browser 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;

以命令方式操作 UI 对于孤立的示例来说效果很好,但在更复杂的系统中管理起来会成倍增加。想象一下更新一个充满不同表单的页面,就像这样。添加新的 UI 元素或新的交互需要仔细检查所有现有代码以确保你没有引入错误(例如,忘记显示或隐藏某些内容)。

¥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).

React 就是为了解决这个问题而生的。

¥React was built to solve this problem.

在 React 中,你不会直接操作 UI - 这意味着你不会直接启用、禁用、显示或隐藏组件。相反,你声明要显示的内容,然后 React 找出如何更新 UI。想想坐上出租车,告诉司机你想去哪里,而不是告诉他们确切的转弯处。送你到那里是司机的工作,他们甚至可能知道一些你没有考虑过的捷径!

¥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!

In a car driven by React, a passenger asks to be taken to a specific place on the map. React figures out how to do that.

Illustrated by Rachel Lee Nabors

以声明方式思考 UI

¥Thinking about UI declaratively

你已经在上面看到了如何命令式地实现一个表单。为了更好地理解如何在 React 中思考,你将在下面的 React 中重新实现这个 UI:

¥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:

  1. 识别组件的不同视觉状态

    ¥Identify your component’s different visual states

  2. 确定触发这些状态变化的因素

    ¥Determine what triggers those state changes

  3. 使用 useState 表示内存中的状态

    ¥Represent the state in memory using useState

  4. 删除任何非必要的状态变量

    ¥Remove any non-essential state variables

  5. 连接事件处理程序以设置状态

    ¥Connect the event handlers to set the state

步骤 1:识别组件的不同视觉状态

¥Step 1: Identify your component’s different visual states

在计算机科学中,你可能听说过 “状态机” 处于几种“状态”之一。如果你与设计师合作,你可能已经看到了不同 “视觉状态” 的模型。React 处于设计和计算机科学的交叉点,因此这两个想法都是灵感的来源。

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

首先,你需要可视化用户可能会看到的 UI 的所有不同 “状态”:

¥First, you need to visualize all the different “states” of the UI the user might see:

  • 空的:表单有一个禁用的 “提交” 按钮。

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

就像设计师一样,你需要在添加逻辑之前为不同的状态 “模拟” 或创建 “模拟”。例如,这里是一个仅针对表单可视部分的模拟。这个模拟由一个名为 status 的属性控制,默认值为 'empty'

¥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':

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>
    </>
  )
}

你可以随意命名该属性,命名并不重要。尝试编辑 status = 'empty'status = 'success' 以查看出现的成功消息。模拟让你可以在连接任何逻辑之前快速迭代 UI。这是同一组件的更充实的原型,仍然是 status 属性的 “受控”:

¥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:

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>
      </>
  );
}

深入研究

一次显示许多视觉状态

¥Displaying many visual states at once

如果一个组件有很多视觉状态,将它们全部显示在一个页面上会很方便:

¥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”.

步骤 2:确定触发这些状态变化的因素

¥Step 2: Determine what triggers those state changes

你可以触发状态更新以响应两种输入:

¥You can trigger state updates in response to two kinds of inputs:

  • 人工输入,例如单击按钮、在字段中输入、导航链接。

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

A finger.
Human inputs
Ones and zeroes.
Computer inputs

Illustrated by Rachel Lee Nabors

在这两种情况下,你都必须设置 状态变量 才能更新 UI。对于你正在开发的表单,你将需要更改状态以响应一些不同的输入:

¥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:

  • 更改文本输入(人类)应将其从空状态切换到打字状态或返回,具体取决于文本框是否为空。

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

注意

请注意,人工输入通常需要 事件处理程序

¥Notice that human inputs often require event handlers!

为了帮助可视化此流程,请尝试在纸上将每个状态绘制为带标签的圆圈,并将两个状态之间的每个变化绘制为箭头。你可以通过这种方式勾勒出许多流程,并在实现之前很久就解决错误。

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

Flow chart moving left to right with 5 nodes. The first node labeled 'empty' has one edge labeled 'start typing' connected to a node labeled 'typing'. That node has one edge labeled 'press submit' connected to a node labeled 'submitting', which has two edges. The left edge is labeled 'network error' connecting to a node labeled 'error'. The right edge is labeled 'network success' connecting to a node labeled 'success'.
Flow chart moving left to right with 5 nodes. The first node labeled 'empty' has one edge labeled 'start typing' connected to a node labeled 'typing'. That node has one edge labeled 'press submit' connected to a node labeled 'submitting', which has two edges. The left edge is labeled 'network error' connecting to a node labeled 'error'. The right edge is labeled 'network success' connecting to a node labeled 'success'.
表格状态

步骤 3:用 useState 表示内存中的状态

¥Step 3: Represent the state in memory with useState

接下来,你需要使用 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!

从绝对必须存在的状态开始。例如,你需要存储输入的 answer,以及存储最后一个错误的 error(如果存在):

¥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:

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:

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!

步骤 4:删除任何非必要的状态变量

¥Step 4: Remove any non-essential state variables

你希望避免状态内容重复,因此你只跟踪必要的内容。花一点时间重构你的状态结构将使你的组件更容易理解,减少重复,并避免意想不到的含义。你的目标是防止内存中的状态不代表你希望用户看到的任何有效 UI 的情况。(例如,你永远不想显示错误消息并同时禁用输入,否则用户将无法更正错误!)

¥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!)

以下是你可以询问的有关状态变量的一些问题:

¥Here are some questions you can ask about your state variables:

  • 这种状态会导致悖论吗?例如,isTypingisSubmitting 不能同时是 true。悖论通常意味着状态没有受到足够的约束。两个布尔值有四种可能的组合,但只有三种对应于有效状态。要删除 “不可能的” 状态,你可以将它们组合成 status,它必须是三个值之一:'typing''submitting''success'

    ¥Does this state cause a paradox? For example, isTyping and isSubmitting can’t both be true. 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 a status that must be one of three values: 'typing', 'submitting', or 'success'.

  • 相同的信息是否已经在另一个状态变量中可用?另一个悖论:isEmptyisTyping 不能同时是 true。通过使它们独立的状态变量,你冒着它们不同步并导致错误的风险。幸运的是,你可以删除 isEmpty 而检查 answer.length === 0

    ¥Is the same information available in another state variable already? Another paradox: isEmpty and isTyping can’t be true at the same time. By making them separate state variables, you risk them going out of sync and causing bugs. Fortunately, you can remove isEmpty and instead check answer.length === 0.

  • 你能从另一个状态变量的反面中得到相同的信息吗?不需要 isError,因为你可以检查 error !== null

    ¥Can you get the same information from the inverse of another state variable? isError is not needed because you can check error !== null instead.

清理之后,你剩下 3 个(从 7 个减少!)基本状态变量:

¥After this clean-up, you’re left with 3 (down from 7!) essential state variables:

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.

深入研究

使用 reducer 消除“不可能”的状态

¥Eliminating “impossible” states with a reducer

这三个变量足以很好地表示此表单的状态。但是,仍然有一些中间状态不完全有意义。例如,当 status'success' 时,非空 error 没有意义。为了更精确地对状态建模,你可以 将其提取到 reducer 中。 Reducer 让你将多个状态变量统一到一个对象中,并整合所有相关逻辑!

¥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!

步骤 5:连接事件处理程序以设置状态

¥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:

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.

回顾

  • 声明式编程意味着为每个视觉状态描述 UI,而不是对 UI 进行微观管理(命令式)。

    ¥Declarative programming means describing the UI for each visual state rather than micromanaging the UI (imperative).

  • 开发组件时:

    ¥When developing a component:

    1. 识别其所有视觉状态。

      ¥Identify all its visual states.

    2. 确定状态变化的人工和电脑触发。

      ¥Determine the human and computer triggers for state changes.

    3. 使用 useState 对状态建模。

      ¥Model the state with useState.

    4. 删除非必要状态以避免错误和悖论。

      ¥Remove non-essential state to avoid bugs and paradoxes.

    5. 连接事件处理程序以设置状态。

      ¥Connect the event handlers to set state.

挑战 1 / 3:
添加和删除 CSS 类

¥Add and remove a CSS class

使单击图片删除外部 <div> 中的 background--active CSS 类,但将 picture--active 类添加到 <img>。再次单击背景应该会恢复原来的 CSS 类。

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

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>
  );
}


React 中文网 - 粤ICP备13048890号