簡單介紹 createContext 跟 useContext 的用途跟使用方法。
createContext 跟 useContext 最常被用來解決的問題就是 props drilling,在開發過程中通常會希望把元件切分的越小越好,以方便共用跟維護,當元件越切越多層的時候,為了共享 state,就會把 state 透過 props 的方式一層層向下傳遞,反而影響了 code 的可讀性。
像上面這樣單純的功能,寫成 code 可能會像下面這樣。
import { useState } from "react";
type Props = {
isLight: boolean;
};
function Card({ isLight }: Props) {
return (
<div style={{ backgroundColor: isLight ? "#ccc" : "#333" }}>
<p style={{ color: isLight ? "#333" : "#ccc" }}>My Card</p>
</div>
);
}
function Board({ isLight }: Props) {
return (
<div style={{ margin: "10px" }}>
<p>Board</p>
<Card isLight={isLight} />
</div>
);
}
function App() {
const [isLight, setIsLight] = useState<boolean>(false);
function toggleLight() {
setIsLight(!isLight);
}
return (
<>
<div style={{ display: "flex" }}>
<Board isLight={isLight} />
</div>
<button onClick={toggleLight}>Toggle Light</button>
</>
);
}
在 App 創建一個 state 然後用 toggleLight 來控制 Card 裡面元素的顏色變化,然後一層層地往下傳讓最裡面的 Card 可以取得這個 state,中間經過的 <Board>
甚至沒有使用到這個變數,只有傳遞 props 的作用,如果專案變大,會越來越難以閱讀跟追蹤 state 的位置。
這個時候可以透過 createContext 跟 useContext 的組合來解決這個 props drilling 的情形。
createContext 就像他的命名一樣,用來建立一個 context 可以讓多個元件存取一個 context。
const SomeContext = createContext(defaultValue)
defaultValue
: 預設 value,當使用 useContext
或是 <SomeContext.Consumer>
的元件並不是 <SomeContext.Provider>
的子元件時就會拿到這個資料,避免出現錯誤。
SomeContext
: 必須要大寫開頭,這是一個 context 物件,裡面有兩個我們需要知道的屬性。
SomeContext.Provider
讓我們把資料提供到子元件。SomeContext.Consumer
讓我們可以取得 SomeContext.Provider 所提供的資料。上面的範例如果改用 createContext 改寫會變成這樣
import { useState, createContext } from "react";
const LightContext = createContext(false);
function Card() {
return (
<LightContext.Consumer>
{(value) => (
<div style={{ backgroundColor: value ? "#ccc" : "#333" }}>
<p style={{ color: value ? "#333" : "#ccc" }}>My Card</p>
</div>
)}
</LightContext.Consumer>
);
}
function Board() {
return (
<div style={{ margin: "10px" }}>
<p>Board</p>
<Card />
</div>
);
}
function App() {
const [isLight, setIsLight] = useState<boolean>(false);
function toggleLight() {
setIsLight(!isLight);
}
return (
<>
<div style={{ display: "flex" }}>
<LightContext.Provider value={isLight}>
<Board />
</LightContext.Provider>
</div>
<button onClick={toggleLight}>Toggle Light</button>
</>
);
}
把你希望獲得資料的元件用 <LightContext.Provider>
包起來,這個 Provider 接收一個 value 屬性,value 就放著你希望共享的資料,這邊我放的是 isLight
。
然後在子元件可以透過 <LightContext.Consumer>
的元件來取得 value,<LightContext.Consumer>
裡面必須要放入一個 render function,而這個 render function 裡面就可以得到 Provider 所提供的 value,但是因為可讀性,現在已經 不建議 用這個方式來取得 value 了,現在都用 useContext 來取得 value。
透過 Context 一樣可以在子元件取得想要的 props 來做到相同的效果,還可以提升 code 的可讀性跟維護性。
用來取得 <LightContext.Provider>
所提供的 value。
const value = useContext(SomeContext)
SomeContext
: 用 createContext
建立的 contextvalue
: <SomeContext.Provider>
所提供的 value
value
改變時 react 會 re-render 被 <SomeContext.Provider>
包起來的所有元件。上面的範例用 useContext 改寫會變成這樣,因為 useContext 其實只是 <SomeContext.Consumer>
的語法糖而已。
function Card() {
const value = useContext(LightContext);
return (
<div style={{ backgroundColor: value ? "#ccc" : "#333" }}>
<p style={{ color: value ? "#333" : "#ccc" }}>My Card</p>
</div>
);
}
使用 useContext 改寫之後整個可讀性有很明顯的上升,不需要使用 <SomeContext.Consumer>
把特定的區域包起來,然後在裡面寫 render function。
context 還有另外一個用途,就是相同的元件可以包裹不同的 Provider 來共享不同的資料或是參照不同的資料。
假設在 main.tsx
的檔案例我建立了一個 SizeContext
物件,並且把 value 傳到整個 <App/>
裡面。
export const SizeContext = createContext(12);
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<SizeContext.Provider value={12}>
<App />
</SizeContext.Provider>
</React.StrictMode>
);
然後在 App.tsx
裡面在不同的元件裡使用 useContext 來取得 value 並且建立不同的 Provider,給其他子元件使用。
import { useContext } from "react";
import { SizeContext } from "./main";
function Card() {
const value = useContext(SizeContext);
return (
<div style={{ border: "solid 1px #000" }}>
<p style={{ fontSize: `${value}px` }}>Card</p>
</div>
);
}
function Board() {
const value = useContext(SizeContext);
return (
<div style={{ border: "solid 1px #000" }}>
<p style={{ fontSize: `${value}px` }}>Board</p>
<SizeContext.Provider value={36}>
<Card />
</SizeContext.Provider>
<Card /> // 這一個元件沒有被 Provider 包起來
</div>
);
}
function App() {
const value = useContext(SizeContext);
return (
<>
<div style={{ border: "solid 1px #000" }}>
<p style={{ fontSize: `${value}px` }}>App</p>
<SizeContext.Provider value={24}>
<Board />
</SizeContext.Provider>
</div>
</>
);
}
畫面會長這樣。
兩個相同的 <Card>
元件一個有被 <Board>
裡面的 Provider
包起來,所以取得到的 context 會不同,導致顯示在畫面上的 font-size 會不同。
如果資料是不會變動的東西時,通常會盡量把資料分開,然後再使用 useContext 取得資料時,可以透過放入不同的 context 來獲取不同的資料,提高可讀性跟維護性。
const userInfo = { name: "Evan", age: "20" };
export const SizeContext = createContext(12);
export const UserContext = createContext(userInfo);
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<UserContext.Provider value={userInfo}>
<SizeContext.Provider value={12}>
<App />
</SizeContext.Provider>
</UserContext.Provider>
</React.StrictMode>
);
createContext - react document
useContext - react document
Passing Data Deeply with Context - react document
下一篇簡單介紹useReducer。
如果內容有誤再麻煩大家指教,我會盡快修改。
這個系列的文章會同步更新在我個人的 Medium,歡迎大家來看看 👋👋👋
Medium