管理状态

中级

随着你的应用的发展,更有意地组织状态以及管理组件之间的数据流会很有帮助。冗余或重复的状态是常见的错误来源。在本章中,你将学习如何良好地构建状态,如何保持状态更新逻辑的可维护性,以及如何在远程组件之间共享状态。

🌐 As your application grows, it helps to be more intentional about how your state is organized and how the data flows between your components. Redundant or duplicate state is a common source of bugs. In this chapter, you’ll learn how to structure your state well, how to keep your state update logic maintainable, and how to share state between distant components.

使用状态对输入做出反应

🌐 Reacting to input with state

使用 React,你不会直接从代码修改 UI。例如,你不会编写“禁用按钮”、“启用按钮”、“显示成功消息”等命令。相反,你会描述在组件的不同视觉状态下你希望看到的 UI(“初始状态”、“输入状态”、“成功状态”),然后根据用户输入触发状态变化。这类似于设计师思考 UI 的方式。

🌐 With React, you won’t modify the UI from code directly. For example, you won’t write commands like “disable the button”, “enable the button”, “show the success message”, etc. Instead, you will describe the UI you want to see for the different visual states of your component (“initial state”, “typing state”, “success state”), and then trigger the state changes in response to user input. This is similar to how designers think about UI.

这是一个使用 React 构建的测验表单。请注意它如何使用 status 状态变量来决定是否启用或禁用提交按钮,以及是否显示成功消息。

🌐 Here is a quiz form built using React. Note how it uses the status state variable to determine whether to enable or disable the submit button, and whether to show the success message instead.

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

准备好学习这个主题了吗?

阅读 用状态响应输入 来学习如何以状态驱动的思维方式处理交互。

🌐 Read Reacting to Input with State to learn how to approach interactions with a state-driven mindset.

阅读更多

选择状态结构

🌐 Choosing the state structure

良好的状态结构可以决定一个组件是易于修改和调试的,还是一直容易出错的。最重要的原则是状态不应包含冗余或重复的信息。如果有不必要的状态,很容易忘记更新它,从而引入错误!

🌐 Structuring state well can make a difference between a component that is pleasant to modify and debug, and one that is a constant source of bugs. The most important principle is that state shouldn’t contain redundant or duplicated information. If there’s unnecessary state, it’s easy to forget to update it, and introduce bugs!

例如,这个表单有一个多余的 fullName 状态变量:

🌐 For example, this form has a redundant fullName state variable:

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
    setFullName(e.target.value + ' ' + lastName);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
    setFullName(firstName + ' ' + e.target.value);
  }

  return (
    <>
      <h2>Let’s check you in</h2>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

你可以在组件渲染时移除它并通过计算 fullName 来简化代码:

🌐 You can remove it and simplify the code by calculating fullName while the component is rendering:

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  const fullName = firstName + ' ' + lastName;

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <h2>Let’s check you in</h2>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

这似乎是一个小改动,但 React 应用中的许多错误都是通过这种方式修复的。

🌐 This might seem like a small change, but many bugs in React apps are fixed this way.

准备好学习这个主题了吗?

阅读 选择国家结构 以了解如何设计国家形态以避免错误。

🌐 Read Choosing the State Structure to learn how to design the state shape to avoid bugs.

阅读更多

在组件之间共享状态

🌐 Sharing state between components

有时,你希望两个组件的状态总是一起变化。要做到这一点,需要从它们两个中移除状态,把状态移动到它们最近的公共父组件,然后通过 props 将状态传递给它们。这被称为“提升状态”,这是编写 React 代码时最常做的事情之一。

🌐 Sometimes, you want the state of two components to always change together. To do it, remove state from both of them, move it to their closest common parent, and then pass it down to them via props. This is known as “lifting state up”, and it’s one of the most common things you will do writing React code.

在这个示例中,每次只能有一个面板处于活动状态。为了实现这一点,与其将活动状态保存在每个单独的面板中,不如让父组件持有状态并为其子组件指定属性。

🌐 In this example, only one panel should be active at a time. To achieve this, instead of keeping the active state inside each individual panel, the parent component holds the state and specifies the props for its children.

import { useState } from 'react';

export default function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel
        title="关于"
        isActive={activeIndex === 0}
        onShow={() => setActiveIndex(0)}
      >
        With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.
      </Panel>
      <Panel
        title="词源"
        isActive={activeIndex === 1}
        onShow={() => setActiveIndex(1)}
      >
        The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple.
      </Panel>
    </>
  );
}

function Panel({
  title,
  children,
  isActive,
  onShow
}) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={onShow}>
          Show
        </button>
      )}
    </section>
  );
}

准备好学习这个主题了吗?

阅读 在组件之间共享状态 来了解如何提升状态以及保持组件同步。

🌐 Read Sharing State Between Components to learn how to lift state up and keep components in sync.

阅读更多

保留和重置状态

🌐 Preserving and resetting state

当你重新渲染组件时,React 需要决定保留(并更新)树的哪些部分,以及丢弃或从头重新创建哪些部分。在大多数情况下,React 的自动行为已经足够好。默认情况下,React 会保留与之前渲染的组件树“匹配”的树的部分。

🌐 When you re-render a component, React needs to decide which parts of the tree to keep (and update), and which parts to discard or re-create from scratch. In most cases, React’s automatic behavior works well enough. By default, React preserves the parts of the tree that “match up” with the previously rendered component tree.

然而,有时这并不是你想要的。在这个聊天应用中,输入消息然后切换收件人不会重置输入内容。这可能会导致用户不小心将消息发送给错误的人:

🌐 However, sometimes this is not what you want. In this chat app, typing a message and then switching the recipient does not reset the input. This can make the user accidentally send a message to the wrong person:

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat contact={to} />
    </div>
  )
}

const contacts = [
  { name: 'Taylor', email: 'taylor@mail.com' },
  { name: 'Alice', email: 'alice@mail.com' },
  { name: 'Bob', email: 'bob@mail.com' }
];

React 允许你覆盖默认行为,并通过传递不同的 key(例如 <Chat key={email} />强制组件重置其状态。这告诉 React,如果接收者不同,它应该被视为一个不同的Chat组件,需要用新的数据(以及像输入框这样的 UI)从头重新创建。现在,在接收者之间切换会重置输入字段——即使你渲染的是同一个组件。

🌐 React lets you override the default behavior, and force a component to reset its state by passing it a different key, like <Chat key={email} />. This tells React that if the recipient is different, it should be considered a different Chat component that needs to be re-created from scratch with the new data (and UI like inputs). Now switching between the recipients resets the input field—even though you render the same component.

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat key={to.email} contact={to} />
    </div>
  )
}

const contacts = [
  { name: 'Taylor', email: 'taylor@mail.com' },
  { name: 'Alice', email: 'alice@mail.com' },
  { name: 'Bob', email: 'bob@mail.com' }
];

准备好学习这个主题了吗?

阅读 保留和重置状态 以了解状态的生命周期以及如何控制它。

🌐 Read Preserving and Resetting State to learn the lifetime of state and how to control it.

阅读更多

将状态逻辑提取到 reducer 中

🌐 Extracting state logic into a reducer

在许多事件处理程序中分散有大量状态更新的组件可能会令人不知所措。对于这些情况,你可以将所有状态更新逻辑集中到组件外部的一个单一函数中,称为“reducer”。你的事件处理程序会变得简洁,因为它们只需要指定用户的“操作”。在文件底部,reducer 函数指定了状态应如何响应每个操作进行更新!

🌐 Components with many state updates spread across many event handlers can get overwhelming. For these cases, you can consolidate all the state update logic outside your component in a single function, called “reducer”. Your event handlers become concise because they only specify the user “actions”. At the bottom of the file, the reducer function specifies how the state should update in response to each action!

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId
    });
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask
        onAddTask={handleAddTask}
      />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

let nextId = 3;
const initialTasks = [
  { id: 0, text: 'Visit Kafka Museum', done: true },
  { id: 1, text: 'Watch a puppet show', done: false },
  { id: 2, text: 'Lennon Wall pic', done: false }
];

准备好学习这个主题了吗?

阅读 将状态逻辑提取到 Reducer,以了解如何在 reducer 函数中整合逻辑。

🌐 Read Extracting State Logic into a Reducer to learn how to consolidate logic in the reducer function.

阅读更多

使用上下文深入传递数据

🌐 Passing data deeply with context

通常,你会通过 props 将信息从父组件传递给子组件。但如果你需要将某个 prop 传递通过许多组件,或者许多组件需要相同的信息,传递 props 可能会变得不方便。Context 允许父组件将某些信息提供给它下面树中的任何组件——无论有多深——而无需通过 props 显式传递。

🌐 Usually, you will pass information from a parent component to a child component via props. But passing props can become inconvenient if you need to pass some prop through many components, or if many components need the same information. Context lets the parent component make some information available to any component in the tree below it—no matter how deep it is—without passing it explicitly through props.

在这里,Heading 组件通过“查询”最近的 Section 来确定其标题级别。每个 Section 通过询问父级 Section 并在其基础上加一来跟踪自己的级别。每个 Section 向其下方的所有组件提供信息,而无需传递 props——它是通过上下文来实现的。

🌐 Here, the Heading component determines its heading level by “asking” the closest Section for its level. Each Section tracks its own level by asking the parent Section and adding one to it. Every Section provides information to all components below it without passing props—it does that through context.

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading>Title</Heading>
      <Section>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Section>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Section>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}

准备好学习这个主题了吗?

阅读 使用 Context 深度传递数据 以了解如何使用上下文作为传递 props 的替代方法。

🌐 Read Passing Data Deeply with Context to learn about using context as an alternative to passing props.

阅读更多

使用 reducer 和上下文进行扩展

🌐 Scaling up with reducer and context

Reducers 让你整合组件的状态更新逻辑。Context 让你将信息传递到深层的其他组件。你可以将 reducers 和 context 结合起来管理复杂界面的状态。

🌐 Reducers let you consolidate a component’s state update logic. Context lets you pass information deep down to other components. You can combine reducers and context together to manage state of a complex screen.

通过这种方法,具有复杂状态的父组件使用 reducer 来管理状态。树中任何深层的其他组件都可以通过上下文读取其状态。它们也可以分发操作来更新该状态。

🌐 With this approach, a parent component with complex state manages it with a reducer. Other components anywhere deep in the tree can read its state via context. They can also dispatch actions to update that state.

import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';

export default function TaskApp() {
  return (
    <TasksProvider>
      <h1>Day off in Kyoto</h1>
      <AddTask />
      <TaskList />
    </TasksProvider>
  );
}

准备好学习这个主题了吗?

阅读 使用 Reducer 和 Context 扩展,了解状态管理如何在不断增长的应用中扩展。

🌐 Read Scaling Up with Reducer and Context to learn how state management scales in a growing app.

阅读更多

下一步是什么?

🌐 What’s next?

前往 Reacting to Input with State 开始逐页阅读本章内容吧!

🌐 Head over to Reacting to Input with State to start reading this chapter page by page!

或者,如果你已经熟悉这些主题,为什么不阅读关于逃生舱的内容呢?

🌐 Or, if you’re already familiar with these topics, why not read about Escape Hatches?