React 16.3 新特性 -- Context API

React在2018年3月29日发布了新版本16.3.0,这一版本带来了很多新特性,让我们一起来学习一下吧~

其实在之前版本的React中就存在context,只不过官方一直不推荐使用。当一个属性需要从最外部的组件传递到最内部的组件中时,使用props逐层传递会让人觉得很不友好,因为中间的组件其实并不需要这个属性,只是因为它内部的组件需要使用而已。此时,可以使用context来避免逐层传递。

context的使用场景

context类似于将变量定义成了组件树中的全局变量,多个组件都可以直接访问该变量。具有这种“全局”性质的数据可以是当前验证的用户信息,主题信息,首选语言等。官方提醒不要为了避免仅有几层的props传递就使用context,而应该坚持只有在数据需要在经过多层传递且在很多组件中使用时才使用context。

context API

React.createContext()

此函数用来创建一个context变量。该变量具有Provider和Consumer两个默认属性。可以传递一个参数用来设置变量的默认值。

const themeContext = React.createContext(defaultValue)
const { Provider, Consumer } = ThemeContext

Provider

Provider用来发布该context变量,使用value属性设置这个值。Provider可以嵌套,内层的Provider会覆盖外层的Provider。

<ThemeContext.Provider value={newValue}>
<OtherComponent>
</ThemeContext>

Consumer

Consumer用来订阅该context变量。Consumer包含在Provider内部,它会从离它最近的父级Provider中获取该变量的值。

Consumer的孩子必须是一个函数,该函数接受从Provider获取到的变量的值作为输入,并返回一个React节点。如果Consumer没有被包含在一个Provider内部,则该函数的输入参数是创建context变量时传入的默认值defaultValue。

当Provider中的value值发生变化的时候,其内部的所有Consumer都会重新渲染。value变化的判断算法使用的是与Object.is()方法一样的sameValue算法。因此当如下这样赋值时,会引起不必要的Consumer的重新渲染

<Provider value={{ key: value }}>

因为当Provider的父级元素重新渲染时,Provider的value会指向一个新创建的对象,尽管值没有发生任何变化,但是引用不同了,所以SameValue算法会返回false,导致Consumer重新渲染。因此,最好将对象类型的变量事先定义在state中,通过变量名来引用。

class App extends React.Component {
constructor (props) {
super(props)
this.state = {
someValue: { key: value }
}
}
render () {
return (
<Provider value={ this.state.someValue }>
<OtherComponent />
</Provider>
)
}
}

Context实践

一键切换按钮的样式

// ThemeContext.js
import React from 'react'
export const themes = {
light: {
foreground: '#000000',
background: '#ffffff'
},
dark: {
foreground: '#ffffff',
background: '#000000'
}
}
export const ThemeContext = React.createContext(themes.dark)
// ThemedButton.js
import React from 'react'
import { ThemeContext } from './ThemeContext.js'
function ThemedButton (props) {
return (
<ThemeContext.Consumer>
{theme => (
<button
{...props}
style={{ backgroundColor: theme.background, color: theme.foreground }}
>
{props.children || 'Context API' }
</button>
)}
</ThemeContext.Consumer>
)
}
export default ThemedButton
// App.js
import React, { Component } from 'react';
import { themes, ThemeContext } from './ThemeContext.js'
import ThemedButton from './ThemedButton.js'
function ToolBar (props) {
return (
<ThemedButton onClick={props.changeTheme}>
change theme
</ThemedButton>
)
}
class App extends Component {
constructor (props) {
super(props)
this.state = {
theme: themes.light
}
}
toggleTheme = () => {
this.setState({
theme: this.state.theme === themes.dark ? themes.light : themes.dark
})
}
render() {
return (
<div>
<ThemeContext.Provider value={this.state.theme}>
<ToolBar changeTheme={this.toggleTheme} />
</ThemeContext.Provider>
<ThemedButton />
</div>
);
}
}
export default App;

通过context传递一个函数

context除了可以传递变量,还可以传递函数,Consumer可以通过传递的函数对context进行更新。

// ThemeContext.js
import React from 'react'
export const themes = {
light: {
foreground: '#000000',
background: '#ffffff'
},
dark: {
foreground: '#ffffff',
background: '#000000'
}
}
export const ThemeContext = React.createContext({
theme: themes.dark,
toggleTheme: () => {}
})
// ThemedButton.js
import React from 'react'
import { ThemeContext } from './Context.js'
function ThemedButton (props) {
return (
<ThemeContext.Consumer>
{({ theme, toggleTheme }) => (
<button
{...props}
style={{ backgroundColor: theme.background, color: theme.foreground }}
onClick={toggleTheme}
>
{props.children || 'Context API' }
</button>
)}
</ThemeContext.Consumer>
)
}
export default ThemedButton
// App.js
import React, { Component } from 'react';
import { themes, ThemeContext } from './Context.js'
import ThemedButton from './ThemedButton.js'
function ToolBar (props) {
return (
<ThemedButton>
toggle theme
</ThemedButton>
)
}
class App extends Component {
constructor (props) {
super(props)
this.state = {
theme: themes.light,
toggleTheme: this.toggleTheme
}
}
toggleTheme = () => {
this.setState({
theme: this.state.theme === themes.dark ? themes.light : themes.dark
})
}
render() {
return (
<ThemeContext.Provider value={this.state}>
<ToolBar />
</ThemeContext.Provider>
);
}
}
export default App;

在生命周期函数中获取context

当想在Consumer的子组件的生命周期函数中应用context变量时,只需将变量通过props传递即可

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 style={{ backgroundColor: theme.background, color: theme.foreground }}>
{children}
</button>
)
}
}
export default props => (
<ThemeContext.Consumer>
{theme => <Button {...props} theme={theme} />}
</ThemeContext.Consumer>
)

利用高阶组件订阅context

类似于主题这种变量,很多组件都可能用到,需要在每个用到的组件外部都套上一层<Context.Consumer>,很明显,手动套的话会多出很多冗余重复的工作,这个时候可以使用高阶组件来简化工作。

function WithTheme (Component) {
return function ThemedComponent (props) {
return (
<ThemeContext.Consumer>
{theme => (
<Component {...props} theme={theme} />
)}
</ThemeContext.Consumer>
)
}
}
function Button (theme, ...rest) {
return (
<button className={theme} {...rest} />
)
}
const ThemedButton = WithTheme(Button)

文章目录
  1. 1. context的使用场景
  2. 2. context API
    1. 2.1. React.createContext()
    2. 2.2. Provider
    3. 2.3. Consumer
  3. 3. Context实践
    1. 3.1. 一键切换按钮的样式
    2. 3.2. 通过context传递一个函数
    3. 3.3. 在生命周期函数中获取context
    4. 3.4. 利用高阶组件订阅context
,