prop drilling 介紹
有一個問題叫做 prop drilling
drill 就是「往下鑽」的意思
範例程式碼如下
Demo.js:
在 Demo
component 裡面,會 render 出 <DemoInner />
在 DemoInner
component 裡面,會 render 出 <DemoInnerBox />
在 DemoInnerBox
component 裡面,會 render 出 <DemoInnerBoxContent />
在 DemoInnerBoxContent
component 裡面,會 render 出 <button>Update title!</button>
這個按鈕
import React, { useState } from "react";
function DemoInnerBoxContent() {
return (
<div>
<button>Update title!</button>
</div>
);
}
function DemoInnerBox() {
return <DemoInnerBoxContent />;
}
function DemoInner() {
return <DemoInnerBox />;
}
export default function Demo() {
const [title, setTitle] = useState("I am title!");
return (
<div>
title: {title}
<DemoInner />
</div>
);
}
在 index.js 會 render 出 Demo
component:
import React from "react";
import ReactDOM from "react-dom";
import Demo from "./Demo";
import reportWebVitals from "./reportWebVitals";
import { ThemeProvider } from "styled-components";
ReactDOM.render(
<ThemeProvider theme={theme}>
<Demo />
</ThemeProvider>,
document.getElementById("root")
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
UI 長這樣:
我要做的功能是:
當我按下在 DemoInnerBoxContent
component 裡面的按鈕,就要去 update 「在 Demo
component 裡面的 title
」
做法是:
當我想要從 children 去 update parent 的 state
時,就要把這個 setTitle
這個 function 從 parent (也就是 DemoInner
component)傳下去
import React, { useState } from "react";
function DemoInnerBoxContent({ setTitle }) {
return (
<div>
<button
onClick={() => {
setTitle(Math.random());
}}
>
Update title!
</button>
</div>
);
}
function DemoInnerBox({ setTitle }) {
return <DemoInnerBoxContent setTitle={setTitle} />;
}
function DemoInner({ setTitle }) {
return <DemoInnerBox setTitle={setTitle} />;
}
export default function Demo() {
const [title, setTitle] = useState("I am title!");
return (
<div>
title: {title}
<DemoInner setTitle={setTitle} />
</div>
);
}
從 DemoInner
component 傳到 DemoInnerBox
component,
再從 DemoInnerBox
component 傳到 DemoInnerBoxContent
component 之後,
我才能在 DemoInnerBoxContent
component 拿到 setTitle
這個 function
遇到的問題是:
我需要傳「很多層」之後,我才可以在 DemoInnerBoxContent
component 拿到 setTitle
這個 function -> 我才能夠呼叫 setTitle
這個 function
這個問題就叫做 prop drilling:
當我的 component 有很多層的時候,我就要傳很多層才能傳到我要的地方,這樣就很麻煩,因為對於中間的那幾層(DemoInner
和 DemoInnerBox
)只是扮演一個中介者的角色,只負責把 prop
向下傳遞而已,它們根本不需要拿到 setTitle
這個 function
而且是,每一層都要傳,不然最後面的會收不到
那有沒有辦法讓我可以從第一層直接傳到「我要的那一層」呢?
這就是 useContext
要解決的問題
useContext 簡介
React 提供了一個方法來解決 prop drilling 的問題,這個方法就是 useContext
context 就是「上下文、脈絡」的意思
useContext
的用法如下:
首先,要先引入兩個東西:useContext
, createContext
import React, { useState, useContext, createContext } from "react";
接著就可以建立一個 context:const TitleContext = createContext()
在 createContext()
裡面,要傳入的是:TitleContext
要提供的 value 初始值
在 React 裡面,可以透過 context 把「上層的東西」傳到下層
在「我要傳遞資料的地方」,用一個 <TitleContext.Provider></TitleContext.Provider>
的 component 包住
意思就是:<TitleContext.Provider></TitleContext.Provider>
這個 component 要提供「Context 的值」
因此,在 value
就可以填入「我要傳下去的 context」,也就是 setTitle
這個 function
-> 在 <TitleContext.Provider value={setTitle}></TitleContext.Provider>
這層 component,把 context 的值設為 setTitle
並傳下去
用 useContext
來使用 context 傳進來的值
在 value
傳入什麼,在 useContext
就會回傳什麼
現在,在 TitleContext.Provider
底下的任何一層(也就是 DemoInner
, DemoInnerBox
, DemoInnerBoxContent
),都可以使用 setTitle
了
在 DemoInnerBoxContent
裡面寫上 const setTitle = useContext(TitleContext)
,意思就是:我在 DemoInnerBoxContent
這層,要使用 TitleContext
這個 context 傳進來的值
透過 context 的方式,這些「中間層」就可以不用再扮演「傳遞 prop
」的角色了
Demo.js:
import React, { useState, useContext, createContext } from "react"; // 先引入 useContext, createContext
const TitleContext = createContext();
function DemoInnerBoxContent() {
const setTitle = useContext(TitleContext); // 我在 DemoInnerBoxContent 這層,要使用 TitleContext 這個 context 傳進來的值
return (
<div>
<button
onClick={() => {
setTitle(Math.random()); // 在這裡就可以使用 setTitle 了
}}
>
Update title!
</button>
</div>
);
}
function DemoInnerBox() {
return <DemoInnerBoxContent />;
}
function DemoInner() {
return <DemoInnerBox />;
}
export default function Demo() {
const [title, setTitle] = useState("I am title!");
// 在 TitleContext.Provider 這層 component,把 context 的值設為 setTitle 並傳下去
return (
<TitleContext.Provider value={setTitle}>
<div>
title: {title}
<DemoInner />
</div>
</TitleContext.Provider>
);
}
在 value
傳下去的 context 可以是任何東西
例如:可以是一個陣列
在 TitleContext.Provider
把 value
設為 [title, setTitle]
傳下去
import React, { useState, useContext, createContext } from "react"; // 先引入 useContext, createContext
const TitleContext = createContext();
function DemoInnerBoxContent() {
const [title, setTitle] = useContext(TitleContext); // 我在 DemoInnerBoxContent 這層,要使用 TitleContext 這個 context 傳進來的值
return (
<div>
<button
onClick={() => {
setTitle(Math.random()); // 在這裡就可以使用 setTitle 了
}}
>
Update title!
</button>
{title}
</div>
);
}
function DemoInnerBox() {
return <DemoInnerBoxContent />;
}
function DemoInner() {
return <DemoInnerBox />;
}
export default function Demo() {
const [title, setTitle] = useState("I am title!");
// 在 TitleContext.Provider 這層 component,把 context 的值設為 setTitle 並傳下去
return (
<TitleContext.Provider value={[title, setTitle]}>
<div>
title: {title}
<DemoInner />
</div>
</TitleContext.Provider>
);
}
用 context 實做出 styled component
styled component 的 <ThemeProvider></ThemeProvider>
,背後就是用 Context 來實作的
index.js:
ReactDOM.render(
<ThemeProvider theme={theme}>
<Demo />
</ThemeProvider>,
document.getElementById("root")
);
自己用 context 模擬實作出 styled component
在 TitleContext.Provider
外面再包一層 ColorContext.Provider
,所以 return 的東西就會同時有兩個 context
同樣地,在 DemoInnerBoxContent
裡面,就可以用 useContext
來使用 ColorContext
了
import React, { useState, useContext, createContext } from "react"; // 先引入 useContext, createContext
const TitleContext = createContext();
const ColorContext = createContext();
function DemoInnerBoxContent() {
const [title, setTitle] = useContext(TitleContext); // 我在 DemoInnerBoxContent 這層,要使用 TitleContext 這個 context 傳進來的值
const colors = useContext(ColorContext); // 使用 ColorContext 這個 context 傳進來的值
return (
<div>
<button
style={{
color: colors.primary, // 在這裡就可以使用 colors 了
}}
onClick={() => {
setTitle(Math.random()); // 在這裡就可以使用 setTitle 了
}}
>
Update title!
</button>
{title}
</div>
);
}
function DemoInnerBox() {
return <DemoInnerBoxContent />;
}
function DemoInner() {
return <DemoInnerBox />;
}
export default function Demo() {
const [title, setTitle] = useState("I am title!");
// 在 TitleContext.Provider 這層 component,把 context 的值設為 setTitle 並傳下去
// 在 ColorContext.Provider 這層 component,把 context 的值設為一個物件並傳下去
return (
<ColorContext.Provider
value={{
primary: "#ff0000",
}}
>
<TitleContext.Provider value={[title, setTitle]}>
<div>
title: {title}
<DemoInner />
</div>
</TitleContext.Provider>
</ColorContext.Provider>
);
}
動態改變傳下去的 context 值
現在,我要做的功能是:
原本按鈕的字是紅色,點擊「Click me」按鈕後,字就會變成藍色
import React, { useState, useContext, createContext } from "react"; // 先引入 useContext, createContext
const ColorContext = createContext();
function DemoInnerBoxContent() {
const colors = useContext(ColorContext); // 使用 ColorContext 這個 context 傳進來的值
return (
<div>
<button
style={{
color: colors.primary, // 在這裡就可以使用 colors.primary 了
}}
onClick={() => {
// setTitle(Math.random()); // 在這裡就可以使用 setTitle 了
}}
>
Update title!
</button>
</div>
);
}
function DemoInnerBox() {
return <DemoInnerBoxContent />;
}
function DemoInner() {
return <DemoInnerBox />;
}
export default function Demo() {
const [title, setTitle] = useState("I am title!");
const [colors, setColors] = useState({
primary: "#ff0000",
});
// 在 ColorContext.Provider 這層 component,把 context 的值設為一個物件並傳下去
return (
<ColorContext.Provider value={colors}>
<div>
<button
onClick={() => {
setColors({
primary: "#0000ff", // 點擊按鈕,就會切換 color
});
}}
>
Click me
</button>
title: {title}
<DemoInner />
</div>
</ColorContext.Provider>
);
}
用這樣子的方式,可以達成「dark theme / light theme」的切換
-> 我用的顏色都是 colors.primary
- 在 dark theme 時,
colors.primary
是黑色 - 在 light theme 時,
colors.primary
是白色
context 的值,是可以在中間層被改變的
我在 Demo
這層,primary 是 "#ff0000" (紅色)
在 DemoInner
這層,primary 還是 "#ff0000" (紅色)
但我在 DemoInnerBox
這層,把 primary 改成 "green" (綠色)
最後,在 DemoInnerBoxContent
這層,primary 就會是 "green" (綠色)
import React, { useState, useContext, createContext } from "react"; // 先引入 useContext, createContext
const ColorContext = createContext();
function DemoInnerBoxContent() {
const colors = useContext(ColorContext); // 使用 ColorContext 這個 context 傳進來的值
return (
<div>
<button
style={{
color: colors.primary, // 在這裡就可以使用 colors.primary 了
}}
onClick={() => {
// setTitle(Math.random()); // 在這裡就可以使用 setTitle 了
}}
>
Update title!
</button>
</div>
);
}
function DemoInnerBox() {
return (
<ColorContext.Provider
value={{
primary: "green",
}}
>
<DemoInnerBoxContent />
</ColorContext.Provider>
);
}
function DemoInner() {
return <DemoInnerBox />;
}
export default function Demo() {
const [title, setTitle] = useState("I am title!");
const [colors, setColors] = useState({
primary: "#ff0000",
});
// 在 ColorContext.Provider 這層 component,把 context 的值設為一個物件並傳下去
return (
<ColorContext.Provider value={colors}>
<div>
<button
onClick={() => {
setColors({
primary: "#0000ff", // 點擊按鈕,就會切換 color
});
}}
>
Click me
</button>
title: {title}
<DemoInner />
</div>
</ColorContext.Provider>
);
}
原因是:
在 Demo
這層,我傳入的 context 是 primary: "#ff0000"
(red)
但是在下面的層,也就是 DemoInnerBox
,又再傳入一個 context 是 primary: "green"
(green)
因此,當我在 DemoInnerBoxContent
使用 ColorContext
時,會「往上層去找距離最近的 context」,所以就會找到 DemoInnerBox
的 context(會是 green)
styled component 的做法
那因為 <ThemeProvider>
是包在最上層,所以裡面的每一層都會拿到一樣的樣式
但其實也可以針對不同的按鈕或 component 去提供不同的 <ThemeProvider>
,讓按鈕有不同的樣式
但通常,一個 App 就只會有一個「統一的 theme」
ReactDOM.render(
<ThemeProvider theme={theme}>
<Demo />
</ThemeProvider>,
document.getElementById("root")
);
useContext 實際使用情境
在一個 App 裡面,底下有很多個 component 都會需要用到同一個 state
假設是一個部落格網站的登入功能
在網站裡面的很多個 component 都會需要用到「使用者的登入狀態」這個 state,才能知道要 render 出什麼相對應的內容
例如:
有登入,才會 render 出「編輯文章的按鈕」
有登入,header 才會 render 出「管理後台的按鈕」
所以我就可以把「使用者的登入狀態」存在最上層的 state
,這樣下層的 component 才可以拿到
有了 context,我就可以不用每一層 component 都傳這個 state
的值,只要在「會需要用到這個 state 的那層」用 useContext
就可以拿到 context 的值了