在上一章節中,我們介紹了 Context 的使用簡介與語法。
React context 的使用環繞三個角色在運作:
另外,我們也介紹了以下幾個 context 語法:
React.creatContext
Context.Provider
Context.Consumer
Class.contextType
Context.displayName
在下一章中,我們將介紹一些 context 的範例以及使用技巧。
現在,讓我們使用實際的範例來了解 context 如何使用。先來了解一下這個範例要達到哪些需求。
現在要做一個小頁面,此頁面可以設定 UI theme。頁面中有兩個 button:
我們如下實作需求:
首先先定義 theme 的種類,這個 app 中會有 light
與 dark
兩種 theme
另外,而此頁面的預設 theme 為 dark
:
const themes = {
light: {
background: "#eeeeee",
color: "#000000"
},
dark: {
background: "#222222",
color: "#ffffff"
}
};
const ThemeContext = React.createContext(
themes.dark // default value
);
創建 context 的語法為 React.createContext()
,參數帶入 theme.dark
代表預設的 theme。
接著製作一個可以隨著 theme 的變化而改變的 button:
class ThemedButton extends React.Component {
render() {
let props = this.props;
let theme = this.context;
return (
<button
{...props}
style={{ backgroundColor: theme.background, color: theme.color }}
/>
);
}
}
ThemedButton.contextType = ThemeContext;
ThemeButton
採用 Class.contextType
語法,將要使用的 context 設定為 ThemeContext
。
因為要把 button 的背景與字體顏色都根據 context 的內容調整,因此在 ThemeButton
中用 this.context
的語法取得 theme 並設為 button 的 backgroundColor
與 color
。
最後就來實作 Toolbar
與最重要的 App
component:
// An intermediate component that uses the ThemedButton
function Toolbar(props) {
return (
<div className="toolbar">
<ThemedButton onClick={props.changeTheme}>Change Theme</ThemedButton>
</div>
);
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
theme: themes.light
};
this.toggleTheme = () => {
this.setState((state) => ({
theme: state.theme === themes.dark ? themes.light : themes.dark
}));
};
}
render() {
// The ThemedButton button inside the ThemeProvider
// uses the theme from state while the one outside uses
// the default dark theme
return (
<div>
<ThemeContext.Provider value={this.state.theme}>
<Toolbar changeTheme={this.toggleTheme} />
</ThemeContext.Provider>
<ThemedButton>Outside Button</ThemedButton>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
Toolbar
職責很簡單,只顯示一個外框,並負責傳遞 changeTheme
的事件。
App
component 則負責:
Provider
將 Toolbar
以下的 component 定為 light
themeToolbar
與外層的 buttontoggleTheme
讀者也可以到 CodePen 上參考完整範例。
從結果上可以看到,Toolbar
內的 button 使用了 Provider 提供的 theme context light
,它會隨著 theme 的切換而改變顏色。相較之下,Toolbar
外的 button 沒有 Provider 包覆,取得的 theme context 會是預設值 dark
,其顏色不會因為 Provider 的 context 值改變而有所變動。
接下來來學習如何從巢狀內的 component 中觸發 context 更新。
讀者應該會有一個疑問,context 確實可以將值不透過中間層的 component 就傳遞到底層的 component,然而如果底層的 component 要觸發更新時要怎麼辦呢?是否還是要將 context 更新函式也透過每一個中間層 component 傳下去呢?這樣是否就失去 context 的作用了呢?
讓巢狀內層的 component 也可不經過中間層 component 就觸發更新的方法其實很簡單,把 context 更新函式也設為 context 內容之一即可。
也就是如果要讓 context 可以被內層 component 改變的話,context 就要有以下的內容:
接著就讓我們來看看實際範例。
接續剛剛的範例,我們有一個可以設定 UI theme 的頁面,然而此頁面只會有一個可以隨著 UI theme 而改變顏色的 button。
但在程式面有另一個需求:
Toolbar
不用傳遞 toggleTheme
prop 也能夠讓最下層的 button 可以改變 theme。實作方式如下,此範例會根據上個範例的內容修改,只會記錄有改動的部分:
首先,修改 ThemeContext
:
// Make sure the shape of the default value passed to
// createContext matches the shape that the consumers expect!
const ThemeContext = React.createContext({
theme: themes.dark,
toggleTheme: () => {},
});
theme 的內容也是一樣會有 light
與 dark
,預設的 theme 也是 dark
。
然而不同的是,因為要讓下層的 button 不經過中間層 component 就可以改變 theme,因此額外把 context 更新函式 toggleTheme
也加到 context 中。
接著修改 ThemeButton
:
class ThemeTogglerButton extends React.Component {
render() {
let props = this.props;
// The Theme Toggler Button receives not only the theme
// but also a toggleTheme function from the context
let { theme, toggleTheme } = this.context;
return (
<button
{...props}
onClick={toggleTheme}
style={{ backgroundColor: theme.background, color: theme.color }}
>
Toggle Theme
</button>
);
}
}
ThemeTogglerButton.contextType = ThemeContext;
現在,button 除了從 context 拿 theme
以外,還會順便拿 toggleTheme
函式。
因為功能增加的關係,把原本的 ThemeButton
改名為 ThemeTogglerButton
。
最後修改 App
component:
class App extends React.Component {
constructor(props) {
super(props);
this.toggleTheme = () => {
this.setState((state) => ({
theme: state.theme === themes.dark ? themes.light : themes.dark
}));
};
// State also contains the updater function so it will
// be passed down into the context provider
this.state = {
theme: themes.light,
toggleTheme: this.toggleTheme
};
}
render() {
// The entire state is passed to the provider
return (
<ThemeContext.Provider value={this.state}>
<Toolbar />
</ThemeContext.Provider>
);
}
}
App
component 唯一修改的部分就是把 state 加上 toggleTheme
後一併傳給 ThemeContext.Provider
,這樣在 Provider 內的 Consumer ThemeTogglerButton
就可以同時收到 theme 的值與 theme 的更新函式了。
讀者也可以到 CodePen 查看完整範例。可以看到按下 button 時,theme context 依然可以正常切換。
在一些需求下,一個 component 可能會要使用多個 context。
舉例來說,一個 Header component 可能會同時需要使用 theme 與 user 的內容。
那實作上,要如何讓一個 component 使用多個 context 呢?
如果要在一個 component 中使用多個不同的 context 只要巢狀使用 Context.Consumer 的語法即可。
舉例來說:
function Page() {
return (
<ThemeContext.Consumer>
{(theme) => (
<UserContext.Consumer>
{(user) => <Content user={user} theme={theme} />}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
要注意的是,Class.contextType
語法只能讓 component 使用一個 context 而已,因此無法支援使用多個 context 的需求。
接著就讓我們來看看實際應用吧!
現在的需求是要做一個頁面,此頁面會有 Header 在最上方,且 Header 要有以下功能:
實作方式如下,首先定義 App 的 context:
// Theme context, default to light theme
const ThemeContext = React.createContext(themes.dark);
// Signed-in user context
const UserContext = React.createContext({
name: "Guest"
});
會有兩個 context:
ThemeContext
:代表 UI theme 的設定,預設為 dark
UserContext
:代表使用者資訊,內容只有 name
屬性而已,預設為 Guest
接著就是製作顯示元件 Profile
與 Header
:
function Profile({ user, theme }) {
return (
<div
style={{
backgroundColor: theme.background,
color: theme.color
}}
>
{user}
</div>
);
}
// A component may consume multiple contexts
function Header() {
return (
<ThemeContext.Consumer>
{(theme) => (
<UserContext.Consumer>
{(user) => <Profile user={user} theme={theme} />}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
Profile
的職責很簡單,就是根據 props 顯示對應的顏色與使用者。
而 Header
就是負責同時使用 ThemeContext
與 UserContext
的地方。
可以看到,因為巢狀的使用 context,所以最內層的 Profile
可以同時拿到 theme 與 user 的內容,這就達到要在一個 component 中使用兩個 context 的需求了。
最後是 App
component:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
theme: themes.light,
signedInUser: "Henry"
};
}
render() {
const { signedInUser, theme } = this.state;
// App component that provides initial context values
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Header />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}
App
則要負責兩件事:
因為現在的 context 有 theme 與 user,因此在 state 中指定完這兩個 context 的值後,就要讓這些 context 值分別放到 ThemeContext.Provider
與 UserContext.Provider
中,才可讓更下層的 Consumer 使用 context。
讀者也可以到 CodePen 上參考完整範例。
另外需要注意的是,如果有兩組 context consumer 經常一起使用,則可以考慮使用 Render Props 的技巧封裝成一個 component,讓重用性更高。
還記得在上一章節中提過,只要 Provider 的 value
改變了,則底下的使用此 Provider 的 Consumer component 必定全部 re-render。
因此在使用 context 時應該避免在 Provider value
中直接新物件。如果每次 render
時,value
的值都是新的物件的話,就會導致下層的所有 Consumer 在也都跟著一起 re-render。在 Consumer 數量多的時候,這會造成很大的問題。
範例如下:
class App extends React.Component {
render() {
return (
<MyContext.Provider value={{something: 'something'}}>
<Toolbar />
</MyContext.Provider>
);
}
}
可以看到每次執行 render
時,MyContext.Provider
的 value
都會是一個新的物件(就算實際上內容沒有改變),而這會導致 Toolbar
裡的 consumer 也隨著 App
的 re-render 而重新渲染。
這個問題的解法就是把 Provider value
搬到 state 中。
如此一來,就算 App
重新 render 了,state 的 instance 依然會是一樣的,如此就可以避免 Provider 改值導致 Consumer 也一起 re-render 的問題了。
解法範例如下:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: {something: 'something'},
};
}
render() {
return (
<Provider value={this.state.value}>
<Toolbar />
</Provider>
);
}
}
在這個章節中,我們用透過範例學習了 context 的實際使用範例與技巧,內容包括:
最後也提到了使用 context 時應該避免 inline 的把值宣告在 Provider 的 value 上,否則可能產生大量Consumer 非預期 re-render 的行為。