教你用 React Context API

2,027 阅读2分钟

Context 简介

这里通过回答如下三个问题来对 Context 进行简单介绍,详细内容可以直接阅读官方文档

Context 是什么?

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

Context 解决了什么问题?

避免显示地通过组件树逐层传递 props 的繁琐操作,可快速在多个组件中共享变量。

Context 最典型的使用场景是什么?

不同层级的组件需要访问相同的数据,例如用户信息、UI主题色、多语言设置等。

典型案例

例如考虑以下的场景:

设计

点击 Header 区域的菜单能够改变整个页面的主题色,点击 Content 区域的按钮能够改变整个页面的语言,达到下面的效果:

实现效果

可以看到,主题颜色和语言在所有模块都被用到了,这个时候用 Context API 是非常合适的。

创建 Context 对象

我们先建一个单独的文件 PageContext 来存储整个页面共享的变量:

import React from 'react'
const PageContext = React.createContext()
export default PageContext

使用 Provider 来提供 Context

在 Page 页面里面,我们把主题色和语言变量,以及改变主题色和改变语言函数,封装成一个完整的上下文,传给 PageContext.Provider 的 value 里面,这样 Page 的所有子组件、子组件的子组件,无论多少层级,都可以使用这个上下文中的变量和函数了:

import { Component } from 'react'
import Header from './Header'
import Body from './Body'
import PageContext from './PageContext'
import Footer from './Footer'
import './Page.css'

class Page extends Component {
  state = { theme: 'red', lang: 'zh' }
  changeTheme = () => {
    this.setState(state => ({ theme: state.theme === 'red' ? 'green' : 'red' }))
  }
  changeLang = () => {
    this.setState(state => ({ lang: state.lang === 'zh' ? 'en' : 'zh' }))
  }
  render() {
    const contextVal = {
      theme: this.state.theme,
      changeTheme: this.changeTheme,
      lang: this.state.lang,
      changeLang: this.changeLang,
    }
    return (
      <PageContext.Provider value={contextVal}>
        <div style={{ width: '500px', border: `1px solid ${contextVal.theme}` }}>
          <Header />
          <Body />
          <Footer />
        </div>
      </PageContext.Provider>
    )
  }
}

export default Page

三种消费 Context 的方式

在子孙组件中,使用上下文变量的方式有两种:

  • 通过 Consumer 组件
  • 通过类组件的静态属性 contextType
  • 通过函数组件的 useContext 钩子

通过 Consumer 组件

在 Menu 区域中,我们定义 Title 组件,通过 Consumer 组件来消费上下文变量:

import { Component } from 'react'
import PageContext from './PageContext'

class Menu extends Component {
  render() {
    return (
      <PageContext.Consumer>
        {ctx => (
          <div style={{ float: 'right', border: `1px solid ${ctx.theme}` }}>
            {ctx.lang === 'zh' ? '菜单' : 'Menu'}
            <button onClick={() => ctx.changeTheme()}>
              {ctx.lang === 'zh' ? '改变主题' : 'change theme'}
            </button>
          </div>
        )}
      </PageContext.Consumer>
    )
  }
}

export default Menu

可以看到,Consumer 组件的孩子必须是一个函数,把上下文变量给传进来了,语法是:

<PageContext.Consumer>
  { ctx => /* 基于 context 值进行渲染*/ }
</PageContext.Consumer>

通过类组件的静态属性 contextType

在 Content 组件中,我们让静态属性 contextType 的值等于 PageContext,那么类的实例就可以用 this.context 来获取上下文了:

import { Component } from 'react'
import PageContext from './PageContext'

class Content extends Component {
  static contextType = PageContext
  render() {
    return (
      <div style={{ flex: 1, border: `1px solid ${this.context.theme}` }}>
        {this.context.lang === 'zh' ? '内容' : 'Content'}
        <button onClick={() => this.context.changeLang()}>
          {this.context.lang === 'zh' ? '改变语言' : 'change lang'}
        </button>
      </div>
    )
  }
}

export default Content

注意,contextType 是固定的属性值,不能写错了,否则 this.context 拿不到上下文。

通过函数组件的 useContext 钩子

在函数组件中是没有 this 的,所以 React 官方提供了 useContext 钩子,我们假想在 Footer 区域有一个 Link 链接,点击也能改变主题,就可以这么写:

import { useContext } from 'react'
import PageContext from './PageContext'

function Link() {
  const context = useContext(PageContext)
  return (
    <div style={{ border: `1px solid ${context.theme}` }}>
      <button onClick={() => context.changeTheme()}>改变主题</button>
    </div>
  )
}

export default Link