React 16.3 | Context API
Context 是用來共享被多元件使用的內容的,例如:使用者資料、介面的 "Theme"... 等;最直接的好處是,可以避免 props 層層傳遞。
-
Before
//以 Navbar 為例 class ExampleNavbar extends React.Component{ constructor(props) { super(props); } /** @overridde */ render() { return ( <div> <ExampleProfileCenter profile={this.props.profile}/> </div> ) } } //可以想成 EUDropdown class ExampleProfileCenter extends React.Component { constructor(props) { super(props); } /** @override */ render() { return ( <div> <ExampleAvater profile={this.props.profile}/> </div> ); } } class ExampleAvatar extends React.Component { constructor(props) { super(props); } /** @override */ render() { return ( <div> <img src={this.props.profile.img}/> <span>{this.props.profile.userName}</span> </div> ); } } -
After
const ProfileContext = React.createContext({ src: "/default-avatar-image.png", name: "Default" }); class ExampleNavbar extends React.Component { constructor(props) { super(props); } render() { return ( <ProfileContext.Provider value={this.props.profile}> <div> <ExampleProfileCenter/> </div> </ProfileContext.Provider> ); } } class ExampleProfileCenter extends React.Component { constructor(props) { super(props); } /** @override */ render() { return ( <div> <ExampleAvater/> </div> ); } } class ExampleAvatar extends React.Component { constructor(props) { super(props); } /** @override */ render() { return ( <ProfileContext.Consumer> {profile => ( <div> <img src={profile.img}/> <span>{profile.userName}</span> </div> )} </ProfileContext.Consumer> ); } }
沒有 Context 的解決方案
使用 component composition;由父元件處理子元件的宣告。
- 把邏輯移至父元件,讓子元件簡化,增加子元件的覆用性。
//以 Navbar 為例
class ExampleNavbar extends React.Component{
constructor(props) {
super(props);
}
/** @overridde */
render() {
const avatar = <ExampleAvatar profile={this.props.profile}/>;
return (
<div>
<ExampleProfileCenter>
{avatar}
</ExampleProfileCenter>
</div>
)
}
}
//可以想成 EUDropdown
class ExampleProfileCenter extends React.Component {
constructor(props) {
super(props);
}
/** @override */
render() {
return (
<div>
<div className="avatar-slot">
{this.props.children}
</div>
</div>
);
}
}
class ExampleAvatar extends React.Component {
constructor(props) {
super(props);
}
/** @override */
render() {
return(
<div>
<img src={this.props.profile.img}/>
<span>{this.props.profile.userName}</span>
</div>
);
}
}
Context 使用
React.createContext API
產生 Provider 及 Consumer
-
接受的參數為 defaultValue,可為任意值
-
僅在 Consumer 父元件不存在 Provider 時作用(見後續介紹)。
passing
undefinedas a Provider value does not cause Consumers to usedefaultValue.
-
class ReactContext {
constructor() {
/** @type {ReactContextProvider} */
this.Provider;
/** @type {ReactContextConsumer} */
this.Consumer;
}
}
/**
* @param {?} defaultValue
* @returns {ReactContext}
*/
React.createContext = defaultValue => {Provider, Consumer}
Provider
設定要傳遞給 Consumer 使用的內容。
- props 僅接受 value,value 為要給子元件使用的內容。
- Provider 提供的內容可同時為複數 Consumer 使用。
- 接受 Provider 提供內容的 Consumer 必須位於 Provider 的子元件 Tree 內。
/*
Provider
|
|-child1
| |
| |-Consumer
| |-可以使用 Provider 的 value
|
|-child2
|
|-child2-1
|-Consumer
|-可以使用 Provider 的 value
*/
<Provider value={/* value */}>
<!-- 使用此 Provider value的子元件們 -->
</Provider>
Consumer
接收父元件 Tree 的 Provider 內容
-
子元件必須為 render function:參數為 Provider 提供的 value;並回傳要渲染的元件。
-
當父元件 Tree 有複數個相同 Context 的 Provider 時,則取最近的 Provider 使用。 -
當父元件 Tree 不存在 Provider 時,則取 createContext 時提供的
defaultValue。 -
當 Provider 的 value 改變時,則 re-render;生命週期受 Provider 控制。
-
由 Provide value 觸發的 re-render,不受
shouldComponentUpdateAPI 管理。 -
為避免不必要的 re-render,最簡單的處理方式是 - 將 this.state 作為 Provider 的 value。
-
如果每次 re-render Provider value 都使用同一個 Object , 則視為 Context 不更新, 渲染則受
shouldComponentUpdate影響。Changes are determined by comparing the new and old values using the same algorithm as
Object.is.The way changes are determined can cause some issues when passing objects as
value: see Caveats.
const ProfileContext = React.createContext({ userName: "FIRST", email: "XXX@DOMAIN", update: () => {} }); class Profile extends React.Component { constructor(props) { super(props); this.wrapperRef = React.createRef(); this.state = { userName: "Maw", email: "maww.hsu@gmail.com", color: "grey", update: state => this.setState(state) }; } changeColor(color) { this.setState({color: color}); this.wrapperRef.current.changeColor("red"); } render() { //NOTE: //this.state.color 與 ProfileContext.Provider.value 為同個 object, //因此 this.state.color 的修改無視子元件的 shouldComponentUpdate, //必定更新. const colorFromContext = <ProfileViewer color={this.state.color}/>; const colorFromOwn = <ProfileViewerWrapper ref={this.wrapperRef}/>; return ( <ProfileContext.Provider value={this.state}> {colorFromOwn} {colorFromContext} <ProfileEditor/> </ProfileContext.Provider> ); } } class ProfileViewerWrapper extends React.Component { constructor(props) { super(props); this.state = { color: "yellow" }; } changeColor(color) { this.setState({color: color}); } render() { return <ProfileViewer color={this.state.color}/>; } } class ProfileViewer extends React.Component { constructor(props) { super(props); } shouldComponentUpdate() { return false; } render() { return ( <ProfileContext.Consumer> {context => ( <div style={{"background": this.props.color}}> <div>{context.userName}</div> <div>{context.email}</div> </div> )} </ProfileContext.Consumer> ); } } class ProfileEditor extends React.Component { constructor(props) { super(props); } render() { return ( <ProfileContext.Consumer> {context => ( <div> <ProfileInput value={context.userName} onChange={e => context.update({userName: e.target.value})} /> <ProfileInput value={context.email} onChange={e => context.update({email: e.target.value})} /> </div> )} </ProfileContext.Consumer> ); } } class ProfileInput extends React.Component { constructor(props) { super(props); } render() { return ( <input value={this.props.value} onChange={this.props.onChange}/> ); } }//如果每次 re-render Context value 都丟同一個 Object 進去, 則視為 Context 不更新, 渲染則受 shouldComponentUpdate 影響 -
<Consumer>
{
/**
* @param {?} val
* @returns {?ReactElement|Array<ReactElement>}
*/
val => ... // 以 Provider 所提供的內容進行渲染
}
</Consumer>
動態 Context
const ThemeContext = React.createContext({
theme: "white",
toggleTheme: () => {}
})
class ExampleNavbar extends React.Component {
constructor(props) {
super(props);
this.toggleTheme = this.toggleTheme.bind(this);
this.state = {
toggle: false,
toggleTheme: this.toggleTheme
}
}
toggleTheme() {
this.setState(prev => ({
toggle: !prev.toggle
}));
}
/** @override */
render() {
return (
<ThemeContext.Provider value={this.state}>
<nav>
<ExampleList/>
</nav>
</ThemeContext.Provider>
)
}
}
class ExampleList extends React.Component {
constructor(props) {
super(props);
}
/** @override */
render() {
return (
<div>
<ExampleButton/>
<ExampleButton/>
</div>
)
}
}
class ExampleButton extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<ThemeContext.Consumer>
{val => (
<button
className={val.theme}
onClick={val.toggleTheme}
>
{Button}
</button>
)}
</ThemeContext.Consumer>
)
}
}
同時套用複數個 Context
<ThemeContext.Provider>
<ProfileContext.Provider>
{/* 內容物 */}
</ProfileContext.Provider>
</ThemeContext.Provider>
<ThemeContext.Consumer>
{theme => (
<ProfileContext.Consumer>
{profile => /* 內容物 */}
</ProfileContext.Consumer>
)}
</ThemeContext.Consumer>
Lifecycle in Context
const ThemeContext = React.createContext("white");
class Button extends React.Component {
componentDidMount() {
// ThemeContext value is this.props.theme
}
componentDidUpdate(prevProps, prevState) {
// Previous ThemeContext value is prevProps.theme
// New ThemeContext value is this.props.theme
}
render() {
const {theme, children} = this.props;
return (
<button className={theme || 'light'}>
{children}
</button>
);
}
}