在早期還沒有Context Provider的時候你每一層都需要靠 props 來傳遞資料,才能達到資料共享。
然後...
於是 Context / Provider 就被創造出來了,一個能夠達成跨過不同輩份,也能達到傳值的工具。
這個東西用到了一個 Compound Pattern 的概念,在之後的章節會提到如何使用,用英文字面上的理解也很簡單:
我的內文內容必須要先被創立才能使用它
比如我先創立 MyContext
的結構內容,那我才能成為提供內容的對象,提供內容的對象在英文就是 Provider,所以你要用 Provider 之前必須成為內容的提供者,使用上來說會是 MyContext.Provider
,這樣符合邏輯和語意的概念就是 Compound Pattern 的精神。
其實這個東西從 class component 就存在了,當你從 React 的 Class Component 轉換到 Functional Component 時,會使用 useContext
Hooks 來拿取對應的 context 內容。
下面我們將講古一個從 Class Component 到 Functional Component 的轉換範例,同時透過一個使用 useContext
的 Hooks,以達到區域性狀態管理。
假設我們有一個簡單的 counter
,我們將示範如何將它從 Class Component 轉換為 Functional Component 並使用 useContext
。
import React, { Component } from 'react';
// 一樣都需要createContext
const CountContext = React.createContext();
class CounterProvider extends Component {
// 早期的寫法
state = {
count: 0,
};
increment = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
// 對也是一樣的用法,這裡算是原汁原味
return (
<CountContext.Provider value={{ count: this.state.count, increment: this.increment }}>
{this.props.children}
</CountContext.Provider>
);
};
};
class Counter extends Component {
// 這裡大概就是唯一的差別了,當你使用static contextType屬性時
// React會自動將最接近的Context.Provider的值分配給this.context
// 但也是這裡最容易讓人搞混
static contextType = CountContext;
render() {
const { count, increment } = this.context;
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
};
};
class App extends Component {
render() {
return (
<CounterProvider>
<div>
<h1>Counter App</h1>
<Counter />
</div>
</CounterProvider>
);
};
};
export default App;
import React, { useContext, useState } from 'react';
const CountContext = React.createContext();
function CounterProvider({ children }) {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
return (
<CountContext.Provider value={{ count, increment }}>
{children}
</CountContext.Provider>
);
}
function Counter() {
// 採用useCountext並帶入CountContext去取到對應的context
const { count, increment } = useContext(CountContext);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
function App() {
return (
<CounterProvider>
<div>
<h1>Counter App</h1>
<Counter />
</div>
</CounterProvider>
);
}
export default App;
如果還需要圖解的話,我強烈推薦這篇
那麼要走出新手村,加上前面也介紹了不少減少重複渲染的方法,我們可以學以致用,來探討怎麼優化我們的Context / Provider。
一般能夠修改的情況我都推薦使用 Redux toolkit,來做到很完善的 global state management,它不僅是將雜亂的 state 做了整合,更重要的是它早就完成了效能上的優化,如果只是想找方法的話也可以考慮 jotai, zustand… ,這類的工具,已經完成優化了,因為接下來的步驟做完你會發現與這類現成的工具有 87% 像。 → 想學 Redux toolkit 請點此
但也許你是一個 Javascript 的老手,想要自己來,那麼我們就來探討一下這些 state management 工具,是如何達成效能優化的。
首先,我們來看一下範例:
import { createContext, useContext, useState } from "react";
const StoreContext = createContext(null);
const TextInput = ({ value }) => {
const [store, setStore] = useContext(StoreContext);
return (
<fieldset className="field">
<legend>{value}: </legend>
<input
value={store[value]}
onChange={(e) => setStore({ ...store, [value]: e.target.value })}
/>
</fieldset>
);
};
const OrderBtns = ({ value }) => {
const [store, setStore] = useContext(StoreContext);
return (
<div
style={{
display: "flex",
justifyContent: "space-around",
alignItems: "center"
}}
>
<button
onClick={() => setStore({ ...store, [value]: (store[value] -= 1) })}
>
-
</button>
<button
onClick={() => setStore({ ...store, [value]: (store[value] += 1) })}
>
+
</button>
</div>
);
};
const InputContainer = () => {
return (
<div className="container">
<h5>輸入面板</h5>
<OrderBtns value="count" />
<TextInput value="name" />
</div>
);
};
const Display = ({ value }) => {
const [store] = useContext(StoreContext);
return (
<div className="value">
{value}: {store[value]}
</div>
);
};
const DisplayContainer = () => {
return (
<div className="container">
<h5>已成立訂單</h5>
<Display value="count" />
<Display value="name" />
</div>
);
};
const ShopContainer = () => {
return (
<div className="container">
<h5>訂單系統</h5>
<InputContainer />
<DisplayContainer />
</div>
);
};
const ContextExapmle = () => {
const store = useState({
count: 0,
name: ""
});
return (
<StoreContext.Provider value={store}>
<h1>coffee shop</h1>
<ShopContainer />
</StoreContext.Provider>
);
};
export default ContextExapmle;
上面是從我去年鐵人賽 Redux 教學的範例拆下來的作法,為一個獨立運作的組件,直接在 app 下引用會看到以下畫面:
功能應該都會正常,如果按加減按鈕的話會影響 count
,input
則直接影響 name
,那麼我們使用 chrome 的 react devtool 來查看渲染情況(沒有安裝devtool但想學的看這篇),會發現不論做哪個動作的按鈕,都會造成全部的組件重新渲染,如下:
input
框輸入字你可能會想說簡單,大不了用 memo 包一包就可以解決了啊!
確實是能解決但不完美,尤其是每個外包層的組件都需要額外用memo處理確實是挺麻煩的,而且最外層的 Provider 還是會因為每次的 state change 造成部分組件重新渲染,memo 解決範例如下:
import { createContext, memo, useContext, useState } from "react";
const StoreContext = createContext(null);
const TextInput = ({ value }) => {
const [store, setStore] = useContext(StoreContext);
return (
<fieldset className="field">
<legend>{value}: </legend>
<input
value={store[value]}
onChange={(e) => setStore({ ...store, [value]: e.target.value })}
/>
</fieldset>
);
};
const OrderBtns = ({ value }) => {
const [store, setStore] = useContext(StoreContext);
return (
<div
style={{
display: "flex",
justifyContent: "space-around",
alignItems: "center"
}}
>
<button
onClick={() => setStore({ ...store, [value]: (store[value] -= 1) })}
>
-
</button>
<button
onClick={() => setStore({ ...store, [value]: (store[value] += 1) })}
>
+
</button>
</div>
);
};
// 加入memo讓他不會被觸發不必要的重新渲染
const InputContainer = memo(() => {
return (
<div className="container">
<h5>輸入面板</h5>
<OrderBtns value="count" />
<TextInput value="name" />
</div>
);
});
const Display = ({ value }) => {
const [store] = useContext(StoreContext);
return (
<div className="value">
{value}: {store[value]}
</div>
);
};
const DisplayContainer = memo(() => {
return (
<div className="container">
<h5>已成立訂單</h5>
<Display value="count" />
<Display value="name" />
</div>
);
});
const ShopContainer = memo(() => {
return (
<div className="container">
<h5>訂單系統</h5>
<InputContainer />
<DisplayContainer />
</div>
);
});
const ContextExapmle = () => {
const store = useState({
count: 0,
name: ""
});
return (
<StoreContext.Provider value={store}>
<h1>coffee shop</h1>
<ShopContainer />
</StoreContext.Provider>
);
};
export default ContextExapmle;
所以,問題還是出在 state change 本身,要想不要觸發的話變成每層都要用memo 的方式處理,或是採用之前提過的 useMemo
或是 useCallback
來處理,也就是說不想這麼麻煩的話就必須採用其他的方法來處理。
這邊留給大家思考的時間,看有沒有辦法想到解決的辦法,下一篇,我再提供我的作法。