有时,你希望两个组件的状态始终一起更改。要做到这一点,从它们两个中删除状态,将其移动到它们最近的共同父级,然后通过属性将其传递给它们。这被称为提升状态,这是你在编写 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.
你将学习到
-
如何通过提升状态在组件之间共享状态
¥How to share state between components by lifting it up
-
什么是受控和非受控组件
¥What are controlled and uncontrolled components
通过示例提升状态
¥Lifting state up by example
在此示例中,父 Accordion
组件渲染两个单独的 Panel
:
¥In this example, a parent Accordion
component renders two separate Panel
s:
-
Accordion
-
Panel
-
Panel
-
每个 Panel
组件都有一个布尔值 isActive
状态,用于确定其内容是否可见。
¥Each Panel
component has a boolean isActive
state that determines whether its content is visible.
按下两个面板的展示按钮:
¥Press the Show button for both panels:
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> </> ); }
注意按下一个面板的按钮不会影响另一个面板 - 它们是独立的。
¥Notice how pressing one panel’s button does not affect the other panel—they are independent.
但是现在假设你想要更改它以便在任何给定时间只展开一个面板。使用该设计,展开第二个面板应该会折叠第一个面板。你会怎么做?
¥But now let’s say you want to change it so that only one panel is expanded at any given time. With that design, expanding the second panel should collapse the first one. How would you do that?
要协调这两个面板,需要分三步 “提升它们的状态” 到一个父组件:
¥To coordinate these two panels, you need to “lift their state up” to a parent component in three steps:
-
从子组件中删除状态。
¥Remove state from the child components.
-
从公共父级传递硬编码数据。
¥Pass hardcoded data from the common parent.
-
将状态添加到公共父级并将其与事件处理程序一起传递。
¥Add state to the common parent and pass it down together with the event handlers.
这将允许 Accordion
组件协调两个 Panel
,并且一次只展开一个。
¥This will allow the Accordion
component to coordinate both Panel
s and only expand one at a time.
步骤 1:从子组件中删除状态
¥Step 1: Remove state from the child components
你会将 Panel
的 isActive
的控制权交给其父组件。这意味着父组件会将 isActive
作为属性传递给 Panel
。首先从 Panel
组件中删除这一行:
¥You will give control of the Panel
’s isActive
to its parent component. This means that the parent component will pass isActive
to Panel
as a prop instead. Start by removing this line from the Panel
component:
const [isActive, setIsActive] = useState(false);
而是,将 isActive
添加到 Panel
的属性列表中:
¥And instead, add isActive
to the Panel
’s list of props:
function Panel({ title, children, isActive }) {
现在 Panel
的父组件可以通过 把它作为属性传递下去 来控制 isActive
。反之,Panel
组件现在无法控制 isActive
的值 - 现在由父组件决定!
¥Now the Panel
’s parent component can control isActive
by passing it down as a prop. Conversely, the Panel
component now has no control over the value of isActive
—it’s now up to the parent component!
步骤 2:从公共父级传递硬编码数据
¥Step 2: Pass hardcoded data from the common parent
要提升状态,你必须找到要协调的两个子组件的最接近公共父组件:
¥To lift state up, you must locate the closest common parent component of both of the child components that you want to coordinate:
-
Accordion
(最近的共同父级)¥
Accordion
(closest common parent)-
Panel
-
Panel
-
在此示例中,它是 Accordion
组件。由于它位于两个面板上方并且可以控制它们的属性,因此它将成为当前面板处于活动状态的 “事实来源”。使 Accordion
组件将硬编码值 isActive
(例如,true
)传递给两个面板:
¥In this example, it’s the Accordion
component. Since it’s above both panels and can control their props, it will become the “source of truth” for which panel is currently active. Make the Accordion
component pass a hardcoded value of isActive
(for example, true
) to both panels:
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
值,然后在屏幕上查看结果。
¥Try editing the hardcoded isActive
values in the Accordion
component and see the result on the screen.
步骤 3:将状态添加到公共父级
¥Step 3: Add state to the common parent
提升状态通常会改变你存储的状态的性质。
¥Lifting state up often changes the nature of what you’re storing as state.
在这种情况下,一次只能激活一个面板。这意味着 Accordion
公共父组件需要跟踪哪个面板是活动面板。它可以使用数字作为状态变量的活动 Panel
的索引,而不是 boolean
值:
¥In this case, only one panel should be active at a time. This means that the Accordion
common parent component needs to keep track of which panel is the active one. Instead of a boolean
value, it could use a number as the index of the active Panel
for the state variable:
const [activeIndex, setActiveIndex] = useState(0);
当 activeIndex
为 0
时,第一个面板处于活动状态,当它为 1
时,为第二个面板。
¥When the activeIndex
is 0
, the first panel is active, and when it’s 1
, it’s the second one.
单击任一 Panel
中的 “展示” 按钮需要更改 Accordion
中的活动索引。Panel
不能直接设置 activeIndex
状态,因为它是在 Accordion
内部定义的。Accordion
组件需要明确允许 Panel
组件通过 将事件处理程序作为属性向下传递 更改其状态:
¥Clicking the “Show” button in either Panel
needs to change the active index in Accordion
. A Panel
can’t set the activeIndex
state directly because it’s defined inside the Accordion
. The Accordion
component needs to explicitly allow the Panel
component to change its state by passing an event handler down as a prop:
<>
<Panel
isActive={activeIndex === 0}
onShow={() => setActiveIndex(0)}
>
...
</Panel>
<Panel
isActive={activeIndex === 1}
onShow={() => setActiveIndex(1)}
>
...
</Panel>
</>
Panel
中的 <button>
现在将使用 onShow
属性作为其点击事件处理程序:
¥The <button>
inside the Panel
will now use the onShow
prop as its click event handler:
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> ); }
这样就完成了状态的提升!将状态移动到公共父组件中允许你协调两个面板。使用活动索引而不是两个 “是否展开” 标志可确保在给定时间只有一个面板处于活动状态。并将事件处理程序传递给子级允许子级更改父级的状态。
¥This completes lifting state up! Moving state into the common parent component allowed you to coordinate the two panels. Using the active index instead of two “is shown” flags ensured that only one panel is active at a given time. And passing down the event handler to the child allowed the child to change the parent’s state.
深入研究
¥Controlled and uncontrolled components
调用具有某些本地状态 “非受控” 的组件是很常见的。例如,带有 isActive
状态变量的原始 Panel
组件不受控制,因为它的父级无法影响面板是否处于活动状态。
¥It is common to call a component with some local state “uncontrolled”. For example, the original Panel
component with an isActive
state variable is uncontrolled because its parent cannot influence whether the panel is active or not.
而是,当组件中的重要信息由属性而不是其自身的本地状态驱动时,你可能会说组件是 “受控”。这让父组件可以完全指定它的行为。带有 isActive
属性的最终 Panel
组件由 Accordion
组件控制。
¥In contrast, you might say a component is “controlled” when the important information in it is driven by props rather than its own local state. This lets the parent component fully specify its behavior. The final Panel
component with the isActive
prop is controlled by the Accordion
component.
不受控制的组件在其父级中更易于使用,因为它们需要的配置更少。但是当你想将它们协调在一起时,它们就不那么灵活了。受控组件具有最大的灵活性,但它们需要父组件使用属性完全配置它们。
¥Uncontrolled components are easier to use within their parents because they require less configuration. But they’re less flexible when you want to coordinate them together. Controlled components are maximally flexible, but they require the parent components to fully configure them with props.
实际上,“受控” 和 “非受控” 并不是严格的技术术语 - 每个组件通常都有本地状态和属性的某种混合。但是,这是讨论组件的设计方式和它们提供的功能的有用方式。
¥In practice, “controlled” and “uncontrolled” aren’t strict technical terms—each component usually has some mix of both local state and props. However, this is a useful way to talk about how components are designed and what capabilities they offer.
在编写组件时,考虑其中哪些信息应该受控(通过属性),哪些信息应该不受控(通过状态)。但是你随时可以改变主意并在以后进行重构。
¥When writing a component, consider which information in it should be controlled (via props), and which information should be uncontrolled (via state). But you can always change your mind and refactor later.
每个状态的单一事实来源
¥A single source of truth for each state
在 React 应用中,许多组件都有自己的状态。某些状态可能 “存在” 接近叶组件(树底部的组件),如输入。其他状态可能 “存在” 更接近应用的顶部。例如,即使是客户端路由库,通常也是通过将当前路由存储在 React 状态中,并通过属性向下传递来实现的!
¥In a React application, many components will have their own state. Some state may “live” close to the leaf components (components at the bottom of the tree) like inputs. Other state may “live” closer to the top of the app. For example, even client-side routing libraries are usually implemented by storing the current route in the React state, and passing it down by props!
对于每个独特的状态,你将选择 “拥有” 的组件。此原则也称为具有 “单一事实来源”。。它并不意味着所有状态都存在于一个地方 - 而是对于每个状态,都有一个特定的组件来保存该信息。与其在组件之间复制共享状态,不如将其提升到它们共同的共享父级,然后将其传递给需要它的子级。
¥For each unique piece of state, you will choose the component that “owns” it. This principle is also known as having a “single source of truth”. It doesn’t mean that all state lives in one place—but that for each piece of state, there is a specific component that holds that piece of information. Instead of duplicating shared state between components, lift it up to their common shared parent, and pass it down to the children that need it.
你的应用会随着你的工作而改变。当你仍在弄清楚状态 “存在” 的每个部分的位置时,你通常会向下移动状态或向后移动状态。这是整个过程的一部分!
¥Your app will change as you work on it. It is common that you will move state down or back up while you’re still figuring out where each piece of the state “lives”. This is all part of the process!
要了解更多组件在实践中的感觉,请阅读 在 React 中思考。
¥To see what this feels like in practice with a few more components, read Thinking in React.
回顾
-
当你想要协调两个组件时,将它们的状态移动到它们的共同父级。
¥When you want to coordinate two components, move their state to their common parent.
-
然后通过它们共同父级的属性将信息传递下去。
¥Then pass the information down through props from their common parent.
-
最后,向下传递事件处理程序,以便子级可以更改父级的状态。
¥Finally, pass the event handlers down so that the children can change the parent’s state.
-
将组件视为 “受控”(由属性驱动)或 “非受控”(由状态驱动)是很有用的。
¥It’s useful to consider components as “controlled” (driven by props) or “uncontrolled” (driven by state).
挑战 1 / 2: 同步输入
¥Synced inputs
这两个输入是独立的。让它们保持同步:编辑一个输入应该用相同的文本更新另一个输入,反之亦然。
¥These two inputs are independent. Make them stay in sync: editing one input should update the other input with the same text, and vice versa.
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> ); }