在组件间共享状态

有时,你希望两个组件的状态始终保持一致。为此,请从这两个组件中移除状态,将其移动到它们最近的共同父组件,然后通过 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 组件。因为它位于两个面板之上并且可以控制它们的属性,所以它将成为当前哪个面板处于活动状态的“唯一数据源”。使 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 组件通过 将事件处理程序作为属性传递 来更改其状态

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

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

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 组件是非受控的,因为它的父组件不能影响面板是否处于活动状态。

相反,当组件中的重要信息由属性而不是其自身的本地状态驱动时,您可以说该组件是“受控的”。这使得父组件可以完全指定其行为。具有 isActive 属性的最终 Panel 组件由 Accordion 组件控制。

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

在实践中,“受控”和“非受控”并不是严格的技术术语——每个组件通常都包含本地状态和属性的混合。但是,这是一种有用的方式来讨论组件是如何设计的以及它们提供了哪些功能。

在编写组件时,请考虑其中的哪些信息应该被控制(通过属性),哪些信息应该是非受控的(通过状态)。但是您始终可以改变主意并在以后重构。

每个状态的唯一数据源

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

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

您的应用程序将在您处理它的过程中发生变化。在您仍在确定每个状态片段“驻留”在哪里的过程中,您通常会向上或向下移动状态。这都是过程的一部分!

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

总结

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

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