组件间状态共享

有时,您希望两个组件的状态始终一起变化。为此,请从这两个组件中移除状态,将其移动到它们最近的公共父组件,然后通过 props 将其传递给它们。这被称为提升状态,这是编写 React 代码中最常见的事情之一。

你将学习

  • 如何通过提升状态来在组件之间共享状态
  • 什么是受控组件和不受控组件

通过示例提升状态

在这个例子中,父 Accordion 组件渲染了两个独立的 Panel

  • Accordion(手风琴)
    • Panel(面板)
    • Panel(面板)

每个 Panel 组件都有一个布尔型 isActive 状态,它决定其内容是否可见。

按下两个面板的“显示”按钮

import { useState } from 'react';

function Panel({ title, children }) {
  const [isActive, setIsActive] = useState(false);
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={() => setIsActive(true)}>
          Show
        </button>
      )}
    </section>
  );
}

export default function Accordion() {
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel title="About">
        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="Etymology">
        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>
    </>
  );
}

注意,按下其中一个面板的按钮不会影响另一个面板——它们是独立的。

Diagram showing a tree of three components, one parent labeled Accordion and two children labeled Panel. Both Panel components contain isActive with value false.
Diagram showing a tree of three components, one parent labeled Accordion and two children labeled Panel. Both Panel components contain isActive with value false.

最初,每个 PanelisActive 状态为 false,因此它们都显示为折叠状态。

The same diagram as the previous, with the isActive of the first child Panel component highlighted indicating a click with the isActive value set to true. The second Panel component still contains value false.
The same diagram as the previous, with the isActive of the first child Panel component highlighted indicating a click with the isActive value set to true. The second Panel component still contains value false.

点击任一 Panel 的按钮只会更新该 PanelisActive 状态。

但是现在假设您想更改它,以便任何时候都只展开一个面板。 采用这种设计,展开第二个面板应该会折叠第一个面板。你会怎么做呢?

要协调这两个面板,您需要通过三个步骤将“状态提升”到父组件。

  1. 移除子组件的状态。
  2. 传递来自公共父组件的硬编码数据。
  3. 添加公共父组件的状态,并将其与事件处理程序一起传递。

这将允许 Accordion 组件协调两个 Panel,并且一次只展开一个。

步骤 1:从子组件中移除状态

您将把对 PanelisActive 的控制权交给其父组件。这意味着父组件将 isActive 作为 prop 传递给 Panel。首先从 Panel 组件中删除此行

const [isActive, setIsActive] = useState(false);

然后,将 isActive 添加到 Panel 的 props 列表中。

function Panel({ title, children, isActive }) {

现在,Panel 的父组件可以通过将其作为 prop 传递控制 isActive。反之,Panel 组件现在对 isActive 的值没有控制权——这现在取决于父组件!

步骤 2:从公共父组件传递硬编码数据

为了提升状态,你必须找到想要协调的两个子组件最近的公共父组件。

  • Accordion (最近的公共父组件)
    • Panel(面板)
    • Panel(面板)

在这个例子中,它是Accordion组件。因为它位于两个面板之上,并且可以控制它们的props,所以它将成为当前哪个面板处于活动状态的“真相来源”。使Accordion组件传递一个硬编码值为isActive(例如,true)给两个面板。

import { useState } from 'react';

export default function Accordion() {
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel title="About" isActive={true}>
        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="Etymology" isActive={true}>
        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 }) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={() => setIsActive(true)}>
          Show
        </button>
      )}
    </section>
  );
}

尝试编辑Accordion组件中硬编码的isActive值,并在屏幕上查看结果。

步骤 3:向公共父组件添加状态

提升状态通常会改变你存储为状态的内容的性质。

在这种情况下,一次只能有一个面板处于活动状态。这意味着Accordion公共父组件需要跟踪哪个面板是活动面板。它可以使用数字作为活动Panel的状态变量的索引,而不是boolean值。

const [activeIndex, setActiveIndex] = useState(0);

activeIndex0时,第一个面板处于活动状态;当它为1时,第二个面板处于活动状态。

单击任一Panel中的“显示”按钮需要更改Accordion中的活动索引。Panel无法直接设置activeIndex状态,因为它是在Accordion内部定义的。Accordion组件需要明确允许Panel组件通过将事件处理程序作为prop传递来更改其状态。

<>
<Panel
isActive={activeIndex === 0}
onShow={() => setActiveIndex(0)}
>
...
</Panel>
<Panel
isActive={activeIndex === 1}
onShow={() => setActiveIndex(1)}
>
...
</Panel>
</>

Panel内部的<button>现在将使用onShow prop作为其点击事件处理程序。

import { useState } from 'react';

export default function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel
        title="About"
        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="Etymology"
        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>
  );
}

这就完成了状态提升!将状态移入公共父组件允许你协调这两个面板。使用活动索引而不是两个“显示”标志确保一次只有一个面板处于活动状态。将事件处理程序传递给子组件允许子组件更改父组件的状态。

Diagram showing a tree of three components, one parent labeled Accordion and two children labeled Panel. Accordion contains an activeIndex value of zero which turns into isActive value of true passed to the first Panel, and isActive value of false passed to the second Panel.
Diagram showing a tree of three components, one parent labeled Accordion and two children labeled Panel. Accordion contains an activeIndex value of zero which turns into isActive value of true passed to the first Panel, and isActive value of false passed to the second Panel.

最初,AccordionactiveIndex0,因此第一个Panel接收isActive = true

The same diagram as the previous, with the activeIndex value of the parent Accordion component highlighted indicating a click with the value changed to one. The flow to both of the children Panel components is also highlighted, and the isActive value passed to each child is set to the opposite: false for the first Panel and true for the second one.
The same diagram as the previous, with the activeIndex value of the parent Accordion component highlighted indicating a click with the value changed to one. The flow to both of the children Panel components is also highlighted, and the isActive value passed to each child is set to the opposite: false for the first Panel and true for the second one.

AccordionactiveIndex状态更改为1时,第二个Panel将改为接收isActive = true

深入探讨

受控组件和非受控组件

通常将具有某些局部状态的组件称为“非受控”组件。例如,具有isActive状态变量的原始Panel组件是非受控的,因为其父组件无法影响面板是否处于活动状态。

相反,当组件中的重要信息由props而不是其自身的局部状态驱动时,你可能会说该组件是“受控”的。这允许父组件完全指定其行为。具有isActive prop的最终Panel组件受Accordion组件控制。

非受控组件在其父组件中更容易使用,因为它们需要较少的配置。但是,当你想将它们协调在一起时,它们的灵活性较差。受控组件具有最大的灵活性,但它们需要父组件使用props对其进行完全配置。

实际上,“受控”和“非受控”并非严格的技术术语——每个组件通常都混合了一些局部状态和props。但是,这是讨论组件设计方式及其提供的功能的一种有用的方法。

编写组件时,请考虑其中哪些信息应该由 (props) 控制,哪些信息应该由 (state) 非受控。但你随时可以改变主意并在以后重构。

每个状态的单一数据源

在 React 应用中,许多组件都有自己的状态。一些状态可能“存在于”接近叶子组件(树底部的组件)的地方,例如输入框。其他状态可能“存在于”更接近应用顶端的地方。例如,即使是客户端路由库通常也是通过将当前路由存储在 React 状态中并通过 props 传递下来来实现的!

对于每一块唯一的状态,你都需要选择“拥有”它的组件。 这个原则也被称为拥有“单一数据源”。 这并不意味着所有状态都存在于一个地方——而是对于每一块状态,都有一个特定的组件来持有这块信息。与其在组件之间复制共享状态,不如将其提升到它们共同的父组件,并传递给需要它的子组件。

你的应用会在你开发过程中不断变化。在你仍然确定每块状态“存在”位置的过程中,移动状态上下是很常见的。这都是开发流程的一部分!

要了解在实践中使用更多组件的感受,请阅读 React 思维方式。

回顾

  • 当你想协调两个组件时,将它们的状态移动到它们的共同父组件。
  • 然后通过 props 从它们的共同父组件传递信息。
  • 最后,传递事件处理程序,以便子组件可以更改父组件的状态。
  • 将组件视为“受控的”(由 props 驱动)或“非受控的”(由 state 驱动)是有用的。

挑战 1 2:
同步输入

这两个输入是独立的。使它们保持同步:编辑一个输入应该使用相同的文本更新另一个输入,反之亦然。

import { useState } from 'react';

export default function SyncedInputs() {
  return (
    <>
      <Input label="First input" />
      <Input label="Second input" />
    </>
  );
}

function Input({ label }) {
  const [text, setText] = useState('');

  function handleChange(e) {
    setText(e.target.value);
  }

  return (
    <label>
      {label}
      {' '}
      <input
        value={text}
        onChange={handleChange}
      />
    </label>
  );
}