React 在组件间共享状态

发布于:2025-04-16 ⋅ 阅读:(28) ⋅ 点赞:(0)

在组件间共享状态

有时候,你希望两个组件的状态始终同步更改。要实现这一点,可以将相关 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 步将状态“提升”到他们的父组件中。

  1. 从子组件中 移除 state 。
  2. 从父组件 传递 硬编码数据。
  3. 为共同的父组件添加 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 向下传递 来更改其状态:

<>
  <Panel
    isActive={activeIndex === 0}
    onShow={() => setActiveIndex(0)}
  >
    ...
  </Panel>
  <Panel
    isActive={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>
            <Panel
                title="未完成打卡地点"
                isActive={activeIndex === 0}
                onShow={() => setActiveIndex(0)}
            >
                <ul>
                    <li>北京故宫</li>
                    <li>北京天安门</li>
                    <li>北京颐和园</li>
                    <li>北京王府井</li>
                </ul>
            </Panel>
            <Panel
                title="已完成打卡地点"
                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>
            ) : (
                <Button
                    variant="solid"
                    color="primary"
                    onClick={onShow}
                >
                    显示
                </Button>
            )}
        </section>
    );
};

在这里插入图片描述
点击下方显示按钮后
在这里插入图片描述
这样,我们就完成了对状态的提升!将状态移至公共父组件中可以让你更好的管理这两个面板。使用激活索引值代替之前的 是否显示 标识确保了一次只能激活一个面板。而通过向下传递事件处理函数可以让子组件修改父组件的状态。
在这里插入图片描述

每个状态都对应唯一的数据源

在 React 应用中,很多组件都有自己的状态。一些状态可能“活跃”在叶子组件(树形结构最底层的组件)附近,例如输入框。另一些状态可能在应用程序顶部“活动”。例如,客户端路由库也是通过将当前路由存储在 React 状态中,利用 props 将状态层层传递下去来实现的!

**对于每个独特的状态,都应该存在且只存在于一个指定的组件中作为 state。**这一原则也被称为拥有 “可信单一数据源”。它并不意味着所有状态都存在一个地方——对每个状态来说,都需要一个特定的组件来保存这些状态信息。你应该 将状态提升 到公共父级,或 将状态传递 到需要它的子级中,而不是在组件之间复制共享的状态。

你的应用会随着你的操作而变化。当你将状态上下移动时,你依然会想要确定每个状态在哪里“活跃”。这都是过程的一部分!

摘要

  • 当你想要整合两个组件时,将它们的 state 移动到共同的父组件中。
  • 然后在父组件中通过 props 把信息传递下去。
  • 最后,向下传递事件处理程序,以便子组件可以改变父组件的 state 。
  • 考虑该将组件视为“受控”(由 prop 驱动)或是“不受控”(由 state 驱动)是十分有益的。