React 在组件间共享状态

在组件间共享状态

有时候,你希望两个组件的状态始终同步更改。要实现这一点,可以将相关 state 从这两个组件上移除,并把 state 放到它们的公共父级,再通过 props 将 state 传递给这两个组件。这被称为"状态提升",这是编写 React 代码时常做的事。

学习内容

  • 如何使用状态提升在组件之间共享状态
  • 什么是受控组件和非受控组件

举例说明一下状态提升

在这个例子中,父组件 Accordion 渲染了 2 个独立的 Panel 组件。

  • Accordion
    • Panel
    • Panel

每个 Panel 组件都有一个布尔值 isActive,用于控制其内容是否可见。

javascript 复制代码
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 组件中 删除下面这一行:

javascript 复制代码
const [isActive, setIsActive] = useState(false);

然后,把 isActive 加入 Panel 组件的 props 中:

javascript 复制代码
const Panel: React.FC<PanelProps> = ({ title, children, isActive}) => { 

现在 Panel 的父组件就可以通过 向下传递 prop 来 控制 isActive。但相反地,Panel 组件对 isActive 的值 没有控制权 ------ 现在完全由父组件决定!

第 2 步: 从公共父组件传递硬编码数据

为了实现状态提升,必须定位到你想协调的 两个 子组件最近的公共父组件:

Accordion (最近的公共父组件)

Panel

Panel

在这个例子中,公共父组件是 Accordion。因为它位于两个面板之上,可以控制它们的 props,所以它将成为当前激活面板的"控制之源"。通过 Accordion 组件将硬编码值 isActive(例如 true )传递给两个面板:

javascript 复制代码
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 值:

javascript 复制代码
const [activeIndex, setActiveIndex] = useState(0);

当 activeIndex 为 0 时,激活第一个面板,为 1 时,激活第二个面板。

在任意一个 Panel 中点击"显示"按钮都需要更改 Accordion 中的激活索引值。 Panel 中无法直接设置状态 activeIndex 的值,因为该状态是在 Accordion 组件内部定义的。 Accordion 组件需要 显式允许 Panel 组件通过 将事件处理程序作为 prop 向下传递 来更改其状态:

javascript 复制代码
<>
  <Panel
    isActive={activeIndex === 0}
    onShow={() => setActiveIndex(0)}
  >
    ...
  </Panel>
  <Panel
    isActive={activeIndex === 1}
    onShow={() => setActiveIndex(1)}
  >
    ...
  </Panel>
</>

现在 Panel 组件中的 将使用 onShow 这个属性作为其点击事件的处理程序:

javascript 复制代码
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 驱动)是十分有益的。
相关推荐
加班是不可能的,除非双倍日工资2 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi2 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip3 小时前
vite和webpack打包结构控制
前端·javascript
excel3 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国3 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼3 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy3 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT4 小时前
promise & async await总结
前端
Jerry说前后端4 小时前
RecyclerView 性能优化:从原理到实践的深度优化方案
android·前端·性能优化
画个太阳作晴天4 小时前
A12预装app
linux·服务器·前端