在组件间共享状态:状态提升与受控组件
在前面几关中,你见证了每个组件拥有的私有独立 State。但很多时候,你希望两个或多个同级组件能够相互配合、步调一致。
例如:
- 一个折叠面板展开时,希望其他折叠面板自动收起;
- 一个搜索输入框输入文字时,下方的地图标记列表同时高亮;
- 两个数字输入框,修改摄氏度时华氏度自动联动换算。
要让同级组件共享数据并产生联动,你必须将它们的状态从子组件中拿出来,提升到它们最近的公共父组件中。这就是著名的 状态提升(Lifting State Up)。
1. 经典场景:制作手风琴折叠面板(Accordion)
想象一个包含两个子面板的页面:
function Panel({ title, children }) {
const [isActive, setIsActive] = useState(false);
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
children
) : (
<button onClick={() => setIsActive(true)}>展开</button>
)}
</section>
);
}
export default function Accordion() {
return (
<>
<h2>历史学者档案</h2>
<Panel title="关于">
阿尔马维尔(Armavir)是亚美尼亚最古老的城市之一。
</Panel>
<Panel title="词源">
这个名字源自亚拉拉特平原古老历史遗迹。
</Panel>
</>
);
}
此时,每个 Panel 都有自己独立的 isActive 状态。点击第一个面板的展开,第一个打开;再点击第二个,第二个也打开。
但如果需求变更为:这是一个严格的手风琴,同一时刻只允许打开一个面板,打开其中一个必须自动合上另一个!
子组件无法互相看到彼此的私有状态。改造开始!
2. 状态提升标准三步走
步骤一:从子组件中移除 State
把子组件原本私有的 isActive 删掉,改为通过 Props 接收外部传入的控制信号:
function Panel({ title, children, isActive, onShow }) {
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
children
) : (
<button onClick={onShow}>展开</button>
)}
</section>
);
}
现在,Panel 变成了一个完全服从上级指挥的组件:让它开就开,点击后它只负责触发 onShow 向上汇报!
步骤二:在公共父组件中声明 State
在共同的父组件 Accordion 中,我们需要记住“当前哪个面板是打开的”。我们用一个数字索引 activeIndex 来记录:
export default function Accordion() {
// 0 代表第一个面板打开,1 代表第二个打开
const [activeIndex, setActiveIndex] = useState(0);
// ...
}
步骤三:把状态与更新回调传递给子组件
父组件根据当前的 activeIndex,分别给两个子组件传入计算好的布尔值和回调函数:
export default function Accordion() {
const [activeIndex, setActiveIndex] = useState(0);
return (
<>
<h2>历史学者档案</h2>
<Panel
title="关于"
isActive={activeIndex === 0}
onShow={() => setActiveIndex(0)}
>
阿尔马维尔(Armavir)是亚美尼亚最古老的城市之一。
</Panel>
<Panel
title="词源"
isActive={activeIndex === 1}
onShow={() => setActiveIndex(1)}
>
这个名字源自亚拉拉特平原古老历史遗迹。
</Panel>
</>
);
}
大功告成!现在只要点击第二个面板的展开,setActiveIndex(1) 执行,父组件重新渲染:
- 第一个面板收到的
isActive变成了0 === 1(即false),自动收起; - 第二个面板收到的
isActive变成了1 === 1(即true),顺利展开!
3. 受控组件 vs 非受控组件
在完成状态提升后,我们顺理成章地接触到了 React 中极其重要的两个术语:
- 受控组件(Controlled Component):
组件的重要信息完全由父组件通过 Props 驱动控制。组件自身不保有最终决策权(比如改造后的
Panel,开不开由父级说了算)。受控组件极具弹性和可预测性,非常容易实现联动和统一管理; - 非受控组件(Uncontrolled Component):
组件内部自己持有私有的状态,父组件无法直接干涉其展现(比如最初那个自带
useState的Panel)。非受控组件在简单孤立场景下使用很省心,但极难与其他组件产生复杂的联动。
在实际开发中,当一个组件需要和外界产生配合时,将其重构为受控组件是标准做法。
总结
- 当多个组件需要同步数据或联动展示时,将状态提升到它们共同的父组件中;
- 状态提升遵循三步:子组件抽离 state 为 props、父组件维护统一 state、父组件向下传递数据与回调;
- 状态提升体现了单一真实数据源(Single Source of Truth)的优雅理念;
- 通过 props 全面控制子组件行为的模式,称为受控组件。