在組件間共享狀態
有時候,你希望兩個組件的狀態始終同步更改。要實現這一點,可以將相關 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 驅動)是十分有益的。