【學習筆記】 Context | React

此篇文章為看過 Scrimba 線上課程 (The Frontend Developer Career Path) 之教學影片後的筆記整理,內容與例子大多出自該教學影片。


簡介

React 中的 component 傳遞是由上往下的,無法在同級的 component 間 / 在其他的分支間傳遞。如果要在同級間傳遞,就要將 state 提升到上一層 component,如果在不同分支,就要一直往上提升到兩個 component 間都有共用的為止。往上提升後,再把 props 一層一層往下傳,傳到天荒地老。

Context 可以解決這種狀況。提供資料的稱為 Provider ,使用資料的為 Consumer。把需要共用的資料包在 Provider 裡,需要調用資料的用 Comsumer 包起來。Consumer 不用通過中間一層一層的傳遞,就可以直接使用 Provider 中的資料。除了 Data , Method 也可以通過這個方法傳遞,如果當某個 components 更新後,需要同時將共用這個 method 的 component 一併更新也可以做到。

使用方式

用 Provider 把 包起來

1
2
3
4
5
6
//創造 context
const Context名字 = React.createContext()

<Context名字.Provider value={"dark"}> //value 是必要的
<App />
</Context名字.Provider>

Consumer 調用 data

class component

  • 方法一 : 在component 外引用
    1
    2
    3
    4
    需要引用資料的 component.contextType = 被創造的 provider

    //例子
    Button.contextType = ThemeContext
  • 方法二 : 在 conponent 內引用 ( render() 前)
    1
    2
    3
    4
    static contextType = 被創造的 provider

    //例子
    static contextType = ThemeContext

function component

1
2
3
<Context名字.Consumer>
function
</Context名字.Consumer>
  • 使用 <Context名字.Consumer> 包起來,裡面必須要是 function
  • 是 render props pattern

🌰 栗子 : 使用 context 處理點擊 button 後轉換主題的效果

基本的 context 架構

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// filename : App.js
// 引入 Header 以及 Button

import React from "react"

import Header from "./Header"
import Button from "./Button"

function App() {
return (
<div>
<Header />
<Button />
</div>
)
}

export default App
1
2
3
4
5
6
// filename : ThemeContext.js
// 創造 context

import React from "react"
const ThemeContext = React.createContext()
export default ThemeContext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// filename : index.js
// 使用 context provider

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import ThemeContext from "./ThemeContext"

ReactDOM.render(
//使用theme context 的 method, Provider 沒有自己的component
//後續會提到怎麼處理
<ThemeContext.Provider value={"dark"}>
<App />
</ThemeContext.Provider>,
document.getElementById('root')
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// filename : Button.js
// 使用 context consumer + 簡單的判斷處理

import React from "react"
import ThemeContext from "./ThemeContext"

function Button(props) {
return (
<ThemeContext.Consumer>
{theme => (
<button className={`${theme}-theme`}>Switch Theme</button>
)}
</ThemeContext.Consumer>
)
}

export default Button
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// filename : Header.js
// 使用 context consumer + 簡單的判斷處理

import React from "react"
import ThemeContext from "./ThemeContext"

function Header (){
return (
<ThemeContext.Consumer>
{theme => (
<header className={`${theme}-theme`}>
<h2>{theme === "light" ? "Light" : "Dark"} Theme</h2>
</header>
)}
</ThemeContext.Consumer>
)
}

export default Header

截至這裡,效果如下。如果將 <ThemeContext.Provider value={"dark"}> 更換成 “light” 就會是 Light Theme. 現在要添加 switch button 切換主題的功能。如果要實現這個功能,需要使用到 state ,但現在 Context Provider 沒有自己的 component,因此要處理這個部分。

把 Context Provider 移到自己的 component

現在要把 Context Provider 移到自己的 component,後續才能在該 component 裡處理 state.

1
2
3
4
5
6
// 原本的
// filename : ThemeContext.js

import React from "react"
const ThemeContext = React.createContext()
export default ThemeContext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 調整後
// filename : ThemeContext.js

import React, { Component } from "react"
const {Provider, Consumer} = React.createContext()

class ThemeContextProvider extends Component {
render() {
return (
<Provider value={"light"}>
{this.props.children}
</Provider>
)
}
}

export {ThemeContextProvider, Consumer as ThemeContextConsumer}

第 7 行 : 創造 ThemeContextProvider component, 這個 component 是之後要在 index.js 中被引用的。index.js 中引入的是 <ThemeContext.Provider> , 所以才這個 component 中要放入 <ThemeContext.Provider>.

第 11 行 : 確保所有 children 都會被 render.

第 5 行 : ThemeContext 本身就帶有 ThemeContext.Provider 以及 ThemeContext.Consumer,因此可以寫成 {Provider, Consumer},

第 10 行 : 同時 return 內的 <ThemeContext.Provider> 就可以只寫成 < Provider >.

第 12 行 : 要把 ThemeContextProvider export 出去,但不能只寫 export default ThemeContextProvider , 這樣只會 export 這個 component. Header.js & Button.js 會用到 ThemeContext,因此要把兩個都 export 出去。

第 17 行 : 在export 時,除了 ThemeContextProvider , Consumer 會以 ThemeContextConsumer export 出去。

由於 ThemeContext.js 中 export 出去的東西改變了,因此其他文件中也要修改。修改完後,結果會與上面的一樣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//filename : Button.js
//Header.js & index.js 同理

import React from "react"
import {ThemeContextConsumer} from "./ThemeContext"

function Button(props) {
return (
<ThemeContextConsumer>
{theme => (
<button className={`${theme}-theme`}>Switch Theme</button>
)}
</ThemeContextConsumer>
)
}

export default Button

修改 context : 增加 state

  • 在 ThemeContext.js 中增加 state 以及轉換 theme 的 function

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    //filename : ThemeContext.js

    import React, { Component } from "react"
    const {Provider, Consumer} = React.createContext()

    class ThemeContextProvider extends Component {
    //增加 state
    state = {
    theme : "light"
    }

    //增加轉換的 function
    toggleTheme = () =>{
    this.setState(prevstate =>{
    return{
    theme : prevstate.theme === "light" ? "dark":"light"
    }
    })
    }

    render() {
    return (
    <Provider value={{theme: this.state.theme, toggleTheme: this.toggleTheme}}> //value 可以傳 object
    {this.props.children}
    </Provider>
    )
    }
    }

    export {ThemeContextProvider, Consumer as ThemeContextConsumer}
  • 把 state 和 function toggleTheme 連接到 Button.js, Header.js 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//filename : Button.js

import React from "react"
import {ThemeContextConsumer} from "./ThemeContext"

function Button(props) {
return (
<ThemeContextConsumer>
{context => (
<button onClick={context.toggleTheme} className={`${context.theme}-theme`}>Switch Theme</button>
)}
</ThemeContextConsumer>
)
}

export default Button

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//filename : Header.js

import React from "react"
import {ThemeContextConsumer} from "./ThemeContext"

function Header (){
return (
<ThemeContextConsumer>
{context => (
<header className={`${context.theme}-theme`}>
<h2>{context.theme === "light" ? "Light" : "Dark"} Theme</h2>
</header>
)}
</ThemeContextConsumer>
)
}

export default Header

🌰 另一個栗子 : 使用 context 處理讓用戶更換 username

基本的 context 架構

1
2
3
4
5
6
//filename : UserContext.js
import React from "react"

//創造 context
const UserContext = React.createContext()
export default UserContext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//filename : index.js

import React from "react"
import ReactDOM from "react-dom"
import "./index.css"
import App from "./App"
import UserContext from "./userContext"

ReactDOM.render(
//把 <App/> 包起來
<UserContext.Provider value={"Luke Skywalker"}>
<App />
</UserContext.Provider>,
document.getElementById("root")
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//filename : App.js
//class component

import React from "react"
import Header from "./Header"
import UserContext from "./userContext"

class App extends React.Component {
//引用
static contextType = UserContext

render() {
// 調用 this.context
// this.context 是 index.js <ThemeContext.Provider>傳進來的 value
const username = this.context
return (
<div>
<Header />
<main>
<p className="main">No new notifications, {username}! 🎉</p>
</main>
</div>
)
}
}

export default App
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//filename : Header.js
//function component

import React, {Component} from "react"
import UserContext from "./userContext"

function Header(props){ //傳進 props
return(
<UserContext.Consumer> //裡面要包 function
{username => (
<header>
<p>Welcome, {username}!</p>
</header>
)}
</UserContext.Consumer>
)
}

export default Header

增加功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//filename :index.js

import React from "react"
import ReactDOM from "react-dom"
import "./index.css"
import App from "./App"
import {UserContextProvider} from "./userContext"

ReactDOM.render(
<UserContextProvider>
<App />
</UserContextProvider>,
document.getElementById("root")
)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//filename : App.js

import React from "react"

import Header from "./Header"
import {UserContextConsumer} from "./userContext"

class App extends React.Component {
state = {
newUsername: ""
}

handleChange = (e) => {
const {name, value} = e.target
this.setState({[name]: value})
}

render() {
return (
<div>
<Header />
<UserContextConsumer>
{({username, changeUsername}) => (
<main>
<p className="main">No new notifications, {username}! 🎉</p>
<input
type="text"
name="newUsername"
placeholder="New username"
value={this.state.newUsername}
onChange={this.handleChange}
/>
<br/>
<button onClick={() => changeUsername(this.state.newUsername)}>Change Username</button>
</main>
)}
</UserContextConsumer>
</div>
)
}
}

export default App

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//filename : userContext.js

import React, { Component } from "react"
const { Provider, Consumer } = React.createContext()

class UserContextProvider extends Component {
state = {
username: "Luke Skywalker"
}

changeUsername = (username) => {
this.setState({username})
}

render () {
const {username} = this.state
return (
<Provider value={{username, changeUsername : this.changeUsername}}>
{this.props.children}
</Provider>
)
}
}


export { UserContextProvider, Consumer as UserContextConsumer }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//filename : Header.js

import React from "react"
import {UserContextConsumer} from "./userContext"

function Header(){
return (
<header>
<UserContextConsumer>
{({username}) => (
<p>Welcome, {username}!</p>
)}
</UserContextConsumer>
</header>
)
}
export default Header

特別情況小栗子 🌰

  • 相同的 component 一個受 Provider 影響,另一個不
    • 不在該 component 本身處理 context
    • 可以在引入該 component 的地方做
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//filename : App.js
import React from "react"

import Header from "./Header"
import Button from "./Button"
import ThemeContext from "./ThemeContext"

function App() {
return (
<div>
<Header />

//這個 button 引用 Provider 資料
<ThemeContext.Consumer>
{theme =>(
<Button theme={theme}/>
)}
</ThemeContext.Consumer>

// 這個 button 不引用
<Button theme="light"/>

</div>
)
}

export default App
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//filename : button.js

import React from "react"
import PropTypes from "prop-types"
import ThemeContext from "./ThemeContext"

function Button(props) {
return (
<button className={`${props.theme}-theme`}>Switch Theme</button>
)
}

Button.propTypes = { //用來限定傳入的 value 局限於這兩個選擇
theme: PropTypes.oneOf(["light", "dark"])
}

Button.defaultProps = {
theme: "light"
}

export default Button

這篇筆記為個人學習記錄,若有錯誤或是可以改進的地方再麻煩各位大大指點(鞠躬

參考資料

上下文(Context)
聊一聊我对 React Context 的理解以及应用

Comments