Context 的各種情境
實際的情境可能更加複雜,如果我們希望切換樣式的按鈕,是底下深層的子元件呢?我可以一次 pass 好幾個 context 下去嗎?React 文件提供了更多情境的範例,給大家參考。
情境一:如何我希望切換狀態的元件,不在父層?
該如何把切換狀態的元件往下傳,從子層做到父層狀態的控制?除了把控制狀態的函式當作 props 一層一層傳遞,這裡也有一個不用層層傳遞的方法:將 toggle method 包在 context 內。
// 建立 context 的時候,先把 toggle method 定義進去
export const ThemeContext = React.createContext({
theme: themes.night,
toggleTheme: () => {},
});
// 將切換的功能與 state 在父層設定好,跟著 theme 一起包著傳下去
...
this.state = {
theme: themes.morning,
toggleTheme: this.toggleTheme,
};
....
render() {
return (
// 這裡傳下去的 value 就包含了樣式與 toggle 樣式的方法
<ThemeContext.Provider value={this.state}>
<ThemeTogglerButton />
</ThemeContext.Provider>
);
}
// 在準備放置切換功能的子元件上,取用隨著 value 傳下來的切換的功能
// * ThemeContext.Consumer 需要放一個函式在子層,這個函式會接收現在的 context,詳細 API 解釋在下方
import {ThemeContext} from './theme-context';
function ThemeTogglerButton() {
return (
<ThemeContext.Consumer>
{({theme, toggleTheme}) => (
<button
onClick={toggleTheme}
style={{backgroundColor: theme.background}}>
Toggle Theme !
</button>
)}
</ThemeContext.Consumer>
);
}
export default ThemeTogglerButton;
情境二:如何我有很多個 context,會打架嗎?
比較複雜的網站可能會有很種類型的 Global 狀態,為了不要拖累 contect re-rendering 的速度,比較好的做法是讓每個 context 區分開來。
如果有些 context 常常被一起使用,你也可以考慮自己設計一個可以同時提供二者的 render prop component(見之後的文章)。
// 假設我有一組共用樣式、一組共用的使用者狀態,我需要分別為他們建立 context
const ThemeContext = React.createContext('morning');
const UserContext = React.createContext({
identity: 'Guest',
});
class App extends React.Component {
render() {
// 在最上層的父元素,將他們一併往下傳,採用層層包覆的寫法
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}
// 這裡是會使用到資料的元件位置
// 分別取到值之後,傳進子元件 <Avatar> 內
function Content() {
return (
<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => (
<Avatar user={user} theme={theme} />
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
情境三:context 的陷阱,以及避開的方法
Context 如何辨識何時該 re-render context 內容?靠的是 reference,所以有時候也會有一些意料之外的情況,當 provider 的父層 re-render 時,意外帶動下層的所有 consumers(訂閱與取用 context 的地方)一起 re-render,因為當父層在 re-render 的時候,會為了 value 不斷重新建立新的物件。
關於辨識 value 值改變的方法,可以參考 Object.is() 以及下方 API 有更完整的說明。
class App extends React.Component {
render() {
return (
// 當 App re-render 的時候,value 的物件也不斷被重新建立
<MyContext.Provider value={{something: 'something'}}>
<Layout />
</MyContext.Provider>
);
}
}
如何解決這種問題?把 value 存放進父層的 state 裡,就可以被保留住。
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
// 把 value 裡的物件存放在這裏,就不會隨著 re-render 被洗掉重建
value: {something: 'something'},
};
}
render() {
return (
// 將 state 直接往下傳
<MyContext.Provider value={this.state.value}>
<Toolbar />
</MyContext.Provider>
);
}
}
Context API - 了解 API 運作方式
const MyContext = React.createContext(defaultValue);
createContext 會建立一個 context 物件,當 React render 訂閱了這個 context 的元件後,會去拿到當下的 context 值(從最接近的上層 Provider 取得)。
createContext 會接收一個參數 defaultValue,只有當元件沒有相對應的上層 Provider 時才會用到。如果在 Provider 內傳入 undefined,並不會導致子元件吃到 defaultValue。
<MyContext.Provider value={/* some value */}>
每個被建造出來的 context 物件,都會有其相對應的 Provider,讓底下的子元件可以訂閱 context 的改變。
Provider 元件會接受一個 value 值,value 值會被傳遞給底下需要使用的元件,一個 Provider 可以有很多個需求方。同樣 Provider 也有可能是很多層、但離子元件越遠的資訊、會被更靠近的覆蓋。
當 value 改變的時候,在 Provider 底下,並且有使用到 context 的元件都會一起 re-render,但這個行為並不會被 shouldComponentUpdate
方法捕捉到。provider 是如何判斷 value 的改變?他會去比較舊值與新值,使用跟 Object.is() 相同的演算法。
class MyClass extends React.Component {
render() {
let value = this.context;
}
}
MyClass.contextType = MyContext;
class.contextType 可以被指派給我們建立出來的 context。使用這個 property,你可以在 class component 內使用 this.context 取得現在的 value,跟底下的 Context.Consumer 不同的是,你可以在生命週期方法裡,使用 this.context 的值。
不過使用這個方法,你只能訂閱一個 context。
// 例如這個例子
function ThemeTogglerButton() {
return (
<ThemeContext.Consumer>
{({theme, toggleTheme}) => (
<button
onClick={toggleTheme}
style={{backgroundColor: theme.background}}>
Toggle Theme
</button>
)}
</ThemeContext.Consumer>
);
}
Consumer 也會去訂閱 context 的改變,需要一個函示放在他的底下,會接受現在的 context 並回傳一個 React node。不同之處在於可以讓你在 function component 內訂閱 context。
如果你使用的是 Hook,請使用 useContext
const value = useContext(MyContext);
useContext 會接收 React.createContext 建立出來的 context 物件,並且 return 當下的 context value,相當於上面提到的 contextType
& Consumer
,不過雖然可以取代這兩個,你還是需要 Provider 把 value 向下傳。