[一文看懂]React的Context与useContext

4,380 阅读6分钟

前言

Context提供了一个无需为每层组件手动添加props,就能在组件树间进行数据传递的方法。Context设计目的是为了共享那些对于一个组件树而言是“全局”的数据。使用context,我们可以避免通过中间元素传递props

context API

  • React.createContext
  • Context.Provider
  • Class.contextType
  • Context.Consumer
  • Context.displayName

何时使用Context

在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

使用Context之前的考虑

Context主要应用场景在于很多不同层级的组件需要访问同样的一些数据。请谨慎使用,因为这会使得组价的复用性变差。

如果你只是想避免层层传递一些属性,组件组合(component composition)有时候是一个比context更好的解决方案。

1. Context(上下文)要解决什么问题?

解决组件间通信的问题,使用context来保存全局状态。有些全局的状态,需要多个组件使用。如果都通过一层层传递的方式来做,会非常麻烦,书写大量的...props会导致逻辑异常的混乱。相信你也曾经历过在一个 层级非常深的组件中获取最外层组件的state,一旦中间某个流程有疏漏,那你的页面就GG了...。这时,我们的Context API就英勇上场了。我们常用的Redux、react-router等第三方库中也重度依赖了Context API.

2. Context的使用场景用哪些?

如上图所示,左边很多个组件需要共享一个全局的上下文数据。作为根节点,用来提供共享的全局上下文数据,从而很方便的共享全局状态。子节点则可以通过Context API来访问这个全局数据,无论是在那一层,无需上一层传递props,子节点可以自己访问。因此,根节点称为provide,子节点称为consume.

代码示例:

const ThemeContext = React.createContext('light')

class App extends React.Component {
    redner () {
        return (
          <ThemeContext.Provider value="dark">
            <ThemedButton />
          </ThemeContext.Provider>
        )
    }
}


// 层级上属于Provider的子组件Consumer
function ThemedButton(props) {
    return (
      <ThemeContext.Consumer>
        {theme => <Button {...props} theme={theme}/>}
      </ThemeContext.Consumer>
    )
}

如何使用 DEMO:

import React from 'react'

const enStrings = {
    submit: 'submit',
    cancel: 'cancel',
}

const cnStrings = {
    submit: '提交',
    cancel: '取消',
}

// 设置默认值
// 1. 使用 createContext 创建上下文
const LocaleContext = React.createContext(enStrings)

// 2. 创建 Provider
class LocaleProvider extend React.Component {
    // 默认状态
    state = { locale: cnStrings }
    // 切换语言
    toggleLocale = () => {
        const locale = this.state.locale === enStrings ? cnStrings : enStrings
        this.setState({ locale })
    }
    
    render() {
        return (
        // state变化,则LocaleContext的value将方法变化,下面使用它的地方将全部变化
        // 在 Provider 中将 state 通过 value 提供出去(根节点)
          <LocaleContext.Provider value={this.state.locale}>
            <button onClick={this.toggleLocale}>
              切换语言
            </button>
            // 将childen显示在此处
            {this.props.children}
          </LocaleContext.Provider>
        )
    }
}

// 3. 创建 Consumer
class LocaledButtons extends React.Component {
    render () {
        return (
          <LocaleContext.Consumer>
          // 函数作为子组件
            {locale => (
              <div>
                <button>{locale.cancel}</button>
                <button>{locale.submit}</button>
              </div>
            )}
          </LocaleContext.Consumer>
        )
    }
}

// 开始使用,不用主动监听变化,react主动监听,并通知子组件自动刷新
export default () => {
    <div>
      <LocalePorvider>
        // children
        <div>
         <LocaledButtons />
        </div>
      </LocalePorvider>
    </div>
}

    1. 创建 LocaleContext
    1. 根据 LocaleContext 创建 LocaleContext.Provider
    1. 根据 LocaleContext 创建 LocaleContext.Consumer

深度使用...

在目前常见的项目如React或TS项目中,我们经常会封装一个service层来进行ajax请求的做法。

<!--ServiceContainer.tsx-->

export class serviceContainer extends Component {
    // 获取接口数据
    getService(config, callback) {
        // 订阅 ajax 可观察对象,触发其观察数据流(否则不会有任何请求)
        const subscriber = this.getApiData(config, callback).subscribe()
    
        // 添加一个 tear down 以便在 subscription 的 unsubscribe() 期间调用
        this.subscription.add(subscriber)
    }
    
    // 获取接口数据
    getApiData(config, callback) {
        this.props.setAjaxStatus('loading') // 标记加载中

        const _config = Object.assign(this.baseAjaxConfig, this.props.ajaxConfig, config)
    
        // 返回一个 ajax 可观察对象
        // 将 res 传递到外部 adapter() 中进一步格式化处理后返回数据
        return this.fetchData(_config).pipe(
          map( (res: any) => this.adapterData(res, callback) )
        )
    }
    
    fetchData (ajaxOptions: AjaxRequest) {
        
    }
    
    componentDidMount () {
        if (!!this.props.isServiceAutoFetch) {
          this.getService()
        }
    }
    
    componentWillUnmount() {
        // 清理 subscription 持有的资源
        this.subscription.unsubscribe()
    }
    
    render () {
        const { Provider } = this.props.context
        const { value } = this.props
    
        // 导出方法函数
        value.getService    = this.getService
        value.getObservable = this.getObservable
        value.resetService  = this.resetService
    
        return (
          <Provider value={value}>
            {this.props.children}
          </Provider>
        )
    }
}

// setContext.tsx

import * as React from 'react'
import { Context, ReactNode } from 'react'

export const setContext = (propName: string, Context: Context<any>) => (Orig: any): any => (props: any): ReactNode => {
  return (
    <Context.Consumer>
      // 还是以函数作为子组件的方式使用
      {(value: any) => <Orig {...props} {...{ [propName]: value }} />}
    </Context.Consumer>
  )
}

// 接口的service
import { createContext } from 'react'
import { ServiceContainer } from '...'

// 初始值
const defaultValue = {

}
// 创建 Context
const context = createContext(defaultValue)

// 服务主体 (xxx代表接口名称)
function xxxService (props) {
  ...
  
 return  (
    <ServiceContainer
      context={context}
      setAjaxStatus={setAjaxStatus}
      setErrorMsg={setErrorMsg}
      resetData={resetData}
      adapter={adapter}
      value={state}
      nationConfig={nationConfig}
      isServiceAutoFetch={isServiceAutoFetch !== false}
      ajaxConfig={ajaxConfig}
    >
      {children}
    </ServiceContainer>
  )
}
// 最终导出
const xxxContext = context
export {
  xxxService,
  xxxrContext,
}

// 使用
import { xxxContext } from '...' // 感觉只是引入了默认值,有什么用...,调用ajax还是在config文件的路由下处理的,因此service与路由有强相关...

useContext

useContext相当于<MyContext.Consumer>class组件中的static contextType = MyContext

useContext(My)只是让你能够读取context的值以及订阅context的变化。你仍然需要在上层组件树中使用<MyContext.Provider>来为下层组件提供context

const value = useContext(MyContext)

接收一个context对象(React.createContext的返回值)并返回该context的当前值。当前的context值由上层组件中距离当前组件最近的<MyContext.Provider>value prop决定。

当组件上层最近的<MyContext.Provider>更新时,该Hook会触发重新渲染,并使用最新传递给MyContext Provider的context value值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

useContext的参数必须是context对象本身:

  • 正确: useContext(MyContext)
  • 错误: useContext(MyContext.Consumer)
  • 错误: useContext(MyContext.Provider)

调用了useContext的组件总会在context值变化时重新渲染。如果重渲染组件的开销较大,你可以 通过使用memoization 来优化

把如下代码与Context.Provider放在一起使用

const themes = {
  light: {
    foreground: '#000000',
    background: '#eeeeee'
  },
  dark: {
    foreground: '#ffffff',
    background: '#222222'
  }
}

const ThemesContext = React.createContext(themes.light)

function App () {
  return (
    <ThemesContext.Provider value={themes.dark}>
      <toolbar />
    </ThemesContext.Provider>
  )
}

function Toolbar(props) {
  return (
    <div>
      <ThemesButton />
    </div>
  )
}

function ThemesButton() {
  const theme = useContext(ThemeContext)
  return (
    <button style={{ background: theme.background, color: theme.foreground}}>
      I am styled by theme context!
    </button>
  )
}

总结:

  1. useContext相当于<MyContext.Consumer>来使用,使用方法如上面的例子。
  2. useContext只能让你能够读取context的值以及订阅context的变化。需要在上层组件树中使用<MyContext.Provider>来为下层组件提供context

综上,可以看到,useContext解决了组件间多层传递props状态值的问题,但是useContext本身并不能改变state的值。而useReducer却可以解决复杂状态下状态值state的管理。因此,可以将全局状态值放入useReducer中,将返回的statedispatch通过<MyContext.Provider>进行传值,这样在子组件中就可以用useContext来接收相应的状态值,并通过dispatch对状态值进行操作。这样,就可以将useContextuseReducer相结合实现Redux的功能!

后记

关于useReducer+useContext如果使用来替代redux,可以参考这篇文章:[一文看懂] useReducer + useContext 如何使用