在组件间共享状态
有时候,你希望两个组件的状态始终同步更改。要实现这一点,可以将相关 state 从这两个组件上移除,并把 state 放到它们的公共父级,再通过 props 将 state 传递给这两个组件。这被称为“状态提升”,这是编写 React 代码时常做的事。
学习内容
- 如何使用状态提升在组件之间共享状态
- 什么是受控组件和非受控组件
举例说明一下状态提升
在这个例子中,父组件 Accordion 渲染了 2 个独立的 Panel 组件。
- Accordion
- Panel
- Panel
每个 Panel 组件都有一个布尔值 isActive,用于控制其内容是否可见。
import React, { useState } from 'react';
import {Button} from 'antd';// Accordion 父组件
const Accordion:React.FC=()=> {return (<><h2>我的旅游清单</h2><Panel title="未完成打卡地点"><ul><li>北京故宫</li><li>北京天安门</li><li>北京颐和园</li><li>北京王府井</li></ul></Panel><Panel title="已完成打卡地点"><ul><li>上海迪士尼</li><li>深圳世界之窗</li><li>广州"小蛮腰"</li><li>广州长隆</li></ul></Panel></>);
}
export default Accordion// 定义 Panel 组件的 props 类型
interface PanelProps {title: string;children: React.ReactNode;
}
// Panel 子组件
const Panel: React.FC<PanelProps>=({title,children})=> {const [isActive, setIsActive] = useState(false);return (<section style={{padding:"10px",background:"#e4e4e4",marginBottom:"10px"}}><h3>{title}</h3>{isActive ? (<p>{children}</p>) : (<Button variant="solid" color="primary" onClick={() => setIsActive(true)}>显示</Button>)}</section>);
}
请点击 2 个面板中的显示按钮:
我们发现点击其中一个面板中的按钮并不会影响另外一个,他们是独立的。
假设现在你想改变这种行为,以便在任何时候只展开一个面板。在这种设计下,展开第 2 个面板应会折叠第 1 个面板。你该如何做到这一点呢?“
要协调好这两个面板,我们需要分 3 步将状态“提升”到他们的父组件中。
- 从子组件中 移除 state 。
- 从父组件 传递 硬编码数据。
- 为共同的父组件添加 state ,并将其与事件处理函数一起向下传递。
这样,Accordion 组件就可以控制 2 个 Panel 组件,保证同一时间只能展开一个。
第 1 步: 从子组件中移除状态
你将把 Panel 组件对 isActive 的控制权交给他们的父组件。这意味着,父组件会将 isActive 作为 prop 传给子组件 Panel。我们先从 Panel 组件中 删除下面这一行:
const [isActive, setIsActive] = useState(false);
然后,把 isActive 加入 Panel 组件的 props 中:
const Panel: React.FC<PanelProps> = ({ title, children, isActive}) => {
现在 Panel 的父组件就可以通过 向下传递 prop 来 控制 isActive。但相反地,Panel 组件对 isActive 的值 没有控制权 —— 现在完全由父组件决定!
第 2 步: 从公共父组件传递硬编码数据
为了实现状态提升,必须定位到你想协调的 两个 子组件最近的公共父组件:
Accordion (最近的公共父组件)
Panel
Panel
在这个例子中,公共父组件是 Accordion。因为它位于两个面板之上,可以控制它们的 props,所以它将成为当前激活面板的“控制之源”。通过 Accordion 组件将硬编码值 isActive(例如 true )传递给两个面板:
import React from 'react';
import {Button} from 'antd';// Accordion 父组件
const Accordion:React.FC=()=> {return (<><h2>我的旅游清单</h2><Panel title="未完成打卡地点" isActive={true}><ul><li>北京故宫</li><li>北京天安门</li><li>北京颐和园</li><li>北京王府井</li></ul></Panel><Panel title="已完成打卡地点" isActive={false}><ul><li>上海迪士尼</li><li>深圳世界之窗</li><li>广州"小蛮腰"</li><li>广州长隆</li></ul></Panel></>);
}
export default Accordion// 定义 Panel 组件的 props 类型
interface PanelProps {title: string;children: React.ReactNode;isActive:boolean
}
// Panel 子组件
const Panel: React.FC<PanelProps>=({title,children,isActive})=> {return (<section style={{padding:"10px",background:"#e4e4e4",marginBottom:"10px"}}><h3>{title}</h3>{isActive ? (<p>{children}</p>) : (<Button variant="solid" color="primary">显示</Button>)}</section>);
}
你可以尝试修改 Accordion 组件中 isActive 的值,并在屏幕上查看结果。
第 3 步: 为公共父组件添加状态
状态提升通常会改变原状态的数据存储类型。
在这个例子中,一次只能激活一个面板。这意味着 Accordion 这个父组件需要记录 哪个 面板是被激活的面板。我们可以用数字作为当前被激活 Panel 的索引,而不是 boolean 值:
const [activeIndex, setActiveIndex] = useState(0);
当 activeIndex 为 0 时,激活第一个面板,为 1 时,激活第二个面板。
在任意一个 Panel 中点击“显示”按钮都需要更改 Accordion 中的激活索引值。 Panel 中无法直接设置状态 activeIndex 的值,因为该状态是在 Accordion 组件内部定义的。 Accordion 组件需要 显式允许 Panel 组件通过 将事件处理程序作为 prop 向下传递 来更改其状态:
<><PanelisActive={activeIndex === 0}onShow={() => setActiveIndex(0)}>...</Panel><PanelisActive={activeIndex === 1}onShow={() => setActiveIndex(1)}>...</Panel>
</>
现在 Panel 组件中的 将使用 onShow 这个属性作为其点击事件的处理程序:
import React, { useState } from 'react';
import {Button} from 'antd';// Accordion 父组件
const Accordion: React.FC = () => {const [activeIndex, setActiveIndex] = useState(0);return (<div><h2 className="text-2xl font-bold mb-4">我的旅游清单</h2><Paneltitle="未完成打卡地点"isActive={activeIndex === 0}onShow={() => setActiveIndex(0)}><ul><li>北京故宫</li><li>北京天安门</li><li>北京颐和园</li><li>北京王府井</li></ul></Panel><Paneltitle="已完成打卡地点"isActive={activeIndex === 1}onShow={() => setActiveIndex(1)}><ul><li>上海迪士尼</li><li>深圳世界之窗</li><li>广州"小蛮腰"</li><li>广州长隆</li></ul></Panel></div>);
};export default Accordion;// 定义 Panel 组件的 props 类型
interface PanelProps {title: string;children: React.ReactNode;isActive: boolean;onShow: () => void;
}// Panel 子组件
const Panel: React.FC<PanelProps> = ({ title, children, isActive, onShow }) => {return (<section style={{padding:"10px",background:"#e4e4e4",marginBottom:"10px"}}><h3 className="text-xl font-bold mb-2">{title}</h3>{isActive ? (<p className="text-gray-700">{children}</p>) : (<Buttonvariant="solid"color="primary"onClick={onShow}>显示</Button>)}</section>);
};
点击下方显示按钮后
这样,我们就完成了对状态的提升!将状态移至公共父组件中可以让你更好的管理这两个面板。使用激活索引值代替之前的 是否显示 标识确保了一次只能激活一个面板。而通过向下传递事件处理函数可以让子组件修改父组件的状态。
每个状态都对应唯一的数据源
在 React 应用中,很多组件都有自己的状态。一些状态可能“活跃”在叶子组件(树形结构最底层的组件)附近,例如输入框。另一些状态可能在应用程序顶部“活动”。例如,客户端路由库也是通过将当前路由存储在 React 状态中,利用 props 将状态层层传递下去来实现的!
**对于每个独特的状态,都应该存在且只存在于一个指定的组件中作为 state。**这一原则也被称为拥有 “可信单一数据源”。它并不意味着所有状态都存在一个地方——对每个状态来说,都需要一个特定的组件来保存这些状态信息。你应该 将状态提升 到公共父级,或 将状态传递 到需要它的子级中,而不是在组件之间复制共享的状态。
你的应用会随着你的操作而变化。当你将状态上下移动时,你依然会想要确定每个状态在哪里“活跃”。这都是过程的一部分!
摘要
- 当你想要整合两个组件时,将它们的 state 移动到共同的父组件中。
- 然后在父组件中通过 props 把信息传递下去。
- 最后,向下传递事件处理程序,以便子组件可以改变父组件的 state 。
- 考虑该将组件视为“受控”(由 prop 驱动)或是“不受控”(由 state 驱动)是十分有益的。