有时候,你希望两个组件的状态始终同步更改。要实现这一点,可以将相关 state 从这两个组件上移除,并把 state 放到它们的公共父级,再通过 props 将 state 传递给这两个组件。这被称为“状态提升”,这是编写 React 代码时常做的事。
你将会学习到
- 如何使用状态提升在组件之间共享状态
- 什么是受控组件和非受控组件
举例说明一下状态提升
在这个例子中,父组件 Accordion
渲染了 2 个独立的 Panel
组件。
Accordion
Panel
Panel
每个 Panel
组件都有一个布尔值 isActive
,用于控制其内容是否可见。
请点击 2 个面板中的显示按钮:
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)}> 显示 </button> )} </section> ); } export default function Accordion() { return ( <> <h2>哈萨克斯坦,阿拉木图</h2> <Panel title="关于"> 阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。 </Panel> <Panel title="词源"> 这个名字来自于 <span lang="kk-KZ">алма</span>,哈萨克语中“苹果”的意思,经常被翻译成“苹果之乡”。事实上,阿拉木图的周边地区被认为是苹果的发源地,<i lang="la">Malus sieversii</i> 被认为是现今苹果的祖先。 </Panel> </> ); }
我们发现点击其中一个面板中的按钮并不会影响另外一个,他们是独立的。
假设现在你想改变这种行为,以便在任何时候只展开一个面板。在这种设计下,展开第 2 个面板应会折叠第 1 个面板。你该如何做到这一点呢?“
要协调好这两个面板,我们需要分 3 步将状态“提升”到他们的父组件中。
- 从子组件中 移除 state 。
- 从父组件 传递 硬编码数据。
- 为共同的父组件添加 state ,并将其与事件处理函数一起向下传递。
这样,Accordion
组件就可以控制 2 个 Panel
组件,保证同一时间只能展开一个。
第 1 步: 从子组件中移除状态
你将把 Panel
组件对 isActive
的控制权交给他们的父组件。这意味着,父组件会将 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>哈萨克斯坦,阿拉木图</h2> <Panel title="关于" isActive={true}> 阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。 </Panel> <Panel title="词源" isActive={true}> 这个名字来自于 <span lang="kk-KZ">алма</span>,哈萨克语中“苹果”的意思,经常被翻译成“苹果之乡”。事实上,阿拉木图的周边地区被认为是苹果的发源地,<i lang="la">Malus sieversii</i> 被认为是现今苹果的祖先。 </Panel> </> ); } function Panel({ title, children, isActive }) { return ( <section className="panel"> <h3>{title}</h3> {isActive ? ( <p>{children}</p> ) : ( <button onClick={() => setIsActive(true)}> 显示 </button> )} </section> ); }
你可以尝试修改 Accordion
组件中 isActive
的值,并在屏幕上查看结果。
第 3 步: 为公共父组件添加状态
状态提升通常会改变原状态的数据存储类型。
在这个例子中,一次只能激活一个面板。这意味着 Accordion
这个父组件需要记录 哪个 面板是被激活的面板。我们可以用数字作为当前被激活 Panel
的索引,而不是 boolean
值:
const [activeIndex, setActiveIndex] = useState(0);
当 activeIndex
为 0
时,激活第一个面板,为 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
这个属性作为其点击事件的处理程序:
import { useState } from 'react'; export default function Accordion() { const [activeIndex, setActiveIndex] = useState(0); return ( <> <h2>哈萨克斯坦,阿拉木图</h2> <Panel title="关于" isActive={activeIndex === 0} onShow={() => setActiveIndex(0)} > 阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。 </Panel> <Panel title="词源" isActive={activeIndex === 1} onShow={() => setActiveIndex(1)} > 这个名字来自于 <span lang="kk-KZ">алма</span>,哈萨克语中“苹果”的意思,经常被翻译成“苹果之乡”。事实上,阿拉木图的周边地区被认为是苹果的发源地,<i lang="la">Malus sieversii</i> 被认为是现今苹果的祖先。 </Panel> </> ); } function Panel({ title, children, isActive, onShow }) { return ( <section className="panel"> <h3>{title}</h3> {isActive ? ( <p>{children}</p> ) : ( <button onClick={onShow}> 显示 </button> )} </section> ); }
这样,我们就完成了对状态的提升!将状态移至公共父组件中可以让你更好的管理这两个面板。使用激活索引值代替之前的 是否显示
标识确保了一次只能激活一个面板。而通过向下传递事件处理函数可以让子组件修改父组件的状态。
深入探讨
通常我们把包含“不受控制”状态的组件称为“非受控组件”。例如,最开始带有 isActive
状态变量的 Panel
组件就是不受控制的,因为其父组件无法控制面板的激活状态。
相反,当组件中的重要信息是由 props
而不是其自身状态驱动时,就可以认为该组件是“受控组件”。这就允许父组件完全指定其行为。最后带有 isActive
属性的 Panel
组件是由 Accordion
组件控制的。
非受控组件通常很简单,因为它们不需要太多配置。但是当你想把它们组合在一起使用时,就不那么灵活了。受控组件具有最大的灵活性,但它们需要父组件使用 props
对其进行配置。
在实践中,“受控”和“非受控”并不是严格的技术术语——通常每个组件都同时拥有内部状态和 props
。然而,这对于组件该如何设计和提供什么样功能的讨论是有帮助的。
当编写一个组件时,你应该考虑哪些信息应该受控制(通过 props
),哪些信息不应该受控制(通过 state
)。当然,你可以随时改变主意并重构代码。
每个状态都对应唯一的数据源
在 React
应用中,很多组件都有自己的状态。一些状态可能“活跃”在叶子组件(树形结构最底层的组件)附近,例如输入框。另一些状态可能在应用程序顶部“活动”。例如,客户端路由库也是通过将当前路由存储在 React
状态中,利用 props
将状态层层传递下去来实现的!
对于每个独特的状态,都应该存在且只存在于一个指定的组件中作为 state。这一原则也被称为拥有 “可信单一数据源”。它并不意味着所有状态都存在一个地方——对每个状态来说,都需要一个特定的组件来保存这些状态信息。你应该 将状态提升 到公共父级,或 将状态传递 到需要它的子级中,而不是在组件之间复制共享的状态。
你的应用会随着你的操作而变化。当你将状态上下移动时,你依然会想要确定每个状态在哪里“活跃”。这都是过程的一部分!
想了解在更多组件中的实践,请阅读 React 哲学.
摘要
- 当你想要整合两个组件时,将它们的 state 移动到共同的父组件中。
- 然后在父组件中通过
props
把信息传递下去。 - 最后,向下传递事件处理程序,以便子组件可以改变父组件的 state 。
- 考虑该将组件视为“受控”(由 prop 驱动)或是“不受控”(由 state 驱动)是十分有益的。
第 1 个挑战 共 2 个挑战: 同步输入状态
现在有两个独立的输入框。为了让它们保持同步:即编辑一个输入框时,另一个输入框也会更新相同的文本,反之亦然。
import { useState } from 'react'; export default function SyncedInputs() { return ( <> <Input label="第一个输入框" /> <Input label="第二个输入框" /> </> ); } function Input({ label }) { const [text, setText] = useState(''); function handleChange(e) { setText(e.target.value); } return ( <label> {label} {' '} <input value={text} onChange={handleChange} /> </label> ); }