【React小书】Context 那些事

前言

当谈及构建现代、可维护的React应用时,React Context成为了一个不可或缺的工具。它为React开发者提供了一种精巧而强大的方式来在组件树中共享状态,避免了繁琐的props传递和回调函数链的问题。

React Context以其简洁的API和出色的性能特性赢得了广泛的认可。但它的魔力不仅仅在于它自身的能力,还在于众多第三方库的生态系统,它们建立在React Context的基础上,为我们提供了更多有趣和实用的功能。一些主流的第三方库,如Redux、Mobx、和React Router,都是基于React Context实现的。Redux提供了一个可预测的状态管理系统,Mobx支持响应式数据流,而React Router则是React生态系统中最受欢迎的路由库之一。

在这篇博客中,我们将深入探讨React Context的核心概念,以及如何使用这些强大的第三方库来加强你的React应用。无论你是React新手还是经验丰富的开发者,React Context都将成为你构建出色应用的有力工具。

抛出问题

我们知道,在不基于任何工具的情况下父组件向子组件传值只能通过props这个途径,那么基于此就有一个问题摆在我们面前了:深层次结构下,父组件如何向子组件传值?,比如下面结构:

xml 复制代码
<FirstComponent>
    <B>
        <C>
            <D>
                ......
                <LastComponent></LastComponent>
            </D>
        </C>
    </B>
</FirstComponent>

如果LastComponent组件需要FirstComponent组件的某个数据,按照之前的说法我们可以使用props;但是我觉得一般人都不会这么做,为什么?一个数据在N个组件中通过Props传递,首先写法上会很冗余、其次就是很可能在某个节点写错了造成最终拿到的数据不是想要的数据,这些都是我们需要考虑的问题。当然有人会想到使用Redux或者Mobx这种第三方库来解决,没毛病;但如果只是一个小小的需求就引入了一个库,是不是杀鸡用了牛刀?在这个问题上React本身有自己的解决方案: Context

Context是什么?

官方对Context的介绍是:

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

意思就是Context提供了一种通过组件树传递数据的方法,而无需在每个级别手动传递props。可以看出这个技术刚好可以用来解决我们前面提出的问题。

Context可以做什么?

事实上官方设计这个API的目的是共享可被视为React组件树的"全局"数据,例如当前经过身份验证的用户,主题或首选语言。意图言简意赅,可以理解成为React组件树(从Root节点开始通过不断组合各个组件形成最终的树形结构)中注入了一个上下文对象同时将一些全局通用的数据放在这个对象中,这样我们就可以在这个组件树的任何地方使用这些数据。

如何使用Context?

针对React Context,官方给我们提供了三个API:

不过呢,后两者其实是React.createContext创建出来的对象的组成,用一段代码来解释吧:

arduino 复制代码
const {Provider, Consumer} = React.createContext(defaultValue);

嗯...就酱紫!!!! 其实写到这里我相信用过Redux的朋友就已经开始觉得眼熟了,就是ProvidercreateContext。因为react-redux提供Provider, Redux提供createStore。这也是Redux基于Context API重要物证哈哈....

当然官方还有提供了一个Hook:useContext,可以让我们在函数组件中方便快捷得访问到Context中的数据。

实战演习

学习技术最终是要有产出的。笔者也一步一步来实现一个简单例子,功能:通过点击按钮对屏幕中数字进行加1操作 首先我们需要创建两个js文件:

js 复制代码
// buildContext.js
import {createContext} from 'react';

const defaultData = {};
export const {Provider, Consumer} = createContext(defaultData);

这里可能有人会有疑问:为什么将创建Context单独抽离出来?

  1. 将Context和组件隔离;因为它们不存在必要的联系,Context只是单纯的注入组件而已。
  2. 因为Provider, Consumer需要配对使用(注意:Provider, Consumer配对使用的前提是它们都来自同一个createContext);我们可以在Provider下的任意节点使用Consumer,所以就可能存在Provider, Consumer不在同一个组件的情况,所以将将创建Context单独抽离出来使得处理Context更加优雅。
jsx 复制代码
// ContextDemo.js
import React, {Component} from 'react'
import {Provider, Consumer} from './buildContext';

class ContextDemo extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
    }

    addOne = () => {
        this.setState((preState) => ({
                    count: preState.count + 1
                }
            )
        )
    };

    render() {
        return (
            <div>
                <Provider value={this.state}>
                    <div>
                        <Consumer>
                            {
                                (context) => <p>{context.count}</p>
                            }
                        </Consumer>
                    </div>
                </Provider>
                <input type="button" value="加1" onClick={this.addOne}/>
            </div>
        )
    }
}
export default ContextDemo

这里我们重点解释下 Provider 与 Consumer:

Provider

被视作一个React组件,它的作用就是接收一个value属性并把它当做最终Context实体注入到Provider的所有子组件中;同时Provider允许被Consumer订阅一个或多个Context的变动,也就是说Provider内部可以有N个Consumer并且它们都可以拿到最新&&相同的Context对象。

如例子所示,我们将组件的State对象注入到Provider字组件中,如果State发生变化那么Provider中的Context对象必定会同步发生变化。

Consumer

依然被视作一个React组件,不过不同的是它的子组件必须是一个方法并且该方法接收当前Context对象并最终返回一个React节点。同时这里有两个问题需要重点关注:

  1. Consumer接收到的Context对象为离它最近的那个Provider注入的Context对象(且必须是通过value属性)。因为Provider作为一个组件也可以进行嵌套。不过笔者认为单独一个React项目最好只存在一个Context对象而且应该作为一个App级的Context对象(也就是将项目的根节点作为Provider的子组件)。这样做笔者认为有两个好处:1)全局只有一个Context更有利于方便使用和管理;2)作为一个App级的Context对象可以让我们在项目的任何一个地方使用到Context对象,发挥Context最大的力量。
  2. 如果Provider不存在(如果存在那么必须要有value属性,否则报错),那么Consumer获取到的Context对象为最初createContext方法的默认参数。

综上所述:Provider的value == Consumer子组件(function)的入参

当我们理解了这两个概念,我们再回过头来看代码; 我们将组件的State(this.state)通过Provider注入到其子组件中,其实可以预料到当我们更改State时候Context对象也会同步变化最终保持一致。所以:

jsx 复制代码
<Consumer>
    {
       (context) => <p>{context.count}</p>
    }
</Consumer>

此时Consumer的子组件(function)的入参context就可以认为是this.state的复制体,所以可以在方法中获取到相应的数据并且在点击按钮更改了State后Context也发生变化,从而实现UI的重新渲染。

小小的测试

前面有句话说:Provider, Consumer配对使用的前提是它们都来自同一个createContext。因此笔者针对这点做了两个实验,目的是测试当Provider, Consumer不是来自同一个createContext会出现什么情况。这里新建两个文件buildContext.js和ContextTest.js

情况一

jsx 复制代码
// buildContext.js
import {createContext} from 'react';

export const {Provider} = createContext({'name': 'Mario'});
export const {Consumer} = createContext({'age': '26'});
jsx 复制代码
ContextTest.js
import React, {Component} from 'react';
import {Provider, Consumer} from "./buildContext";

class Context extends Component {
    render() {
        return (
            <Provider>{/*name*/}
                <Consumer>{/*age*/}
                    {
                        (context) => (
                            <div>
                                <p>age: {context.age}</p>
                                <p>name: {context.name}</p>
                            </div>
                        )
                    }
                </Consumer>
            </Provider>
        )
    }
}

export default Context;

实际运行结果

运行的结果是否有点意想不到? Consumer拿到的Context并不是离它最近的Provider提供的,而是创造它的createContext方法的默认值,即:export const {Consumer} = createContext({'age': '26'});

情况二

jsx 复制代码
// buildContext.js
import {createContext} from 'react';

export const NameContext = createContext({'name': 'Mario'});
export const AgeContext = createContext({'age': '26'});
jsx 复制代码
// ContextTest.js
import React, {Component} from 'react';
import {NameContext, AgeContext} from "./buildContext";

class Context extends Component {
    render() {
        return (
            <NameContext.Provider value={{'name':'React'}}>{/*name*/}
                <AgeContext.Consumer>{/*age*/}
                    {
                        (context) => (
                            <div>
                                <p>age: {context.age}</p>
                                <p>name: {context.name}</p>
                            </div>
                        )
                    }
                </AgeContext.Consumer>
            </NameContext.Provider>
        )
    }
}

export default Context;;

这里我们给Provider提供一个value属性;最终运行结果与情况一一致

结论

因此我们可以猜测:如果Provider, Consumer不是来自同一个createContext,那么Consumer获取到的Context则是自己的createContext方法的默认值,此时的Provider被视为不存在。

相关推荐
一路向前的月光4 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   4 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web4 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
Jiaberrr5 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
安冬的码畜日常7 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
太阳花ˉ7 小时前
html+css+js实现step进度条效果
javascript·css·html
john_hjy8 小时前
11. 异步编程
运维·服务器·javascript
风清扬_jd8 小时前
Chromium 中JavaScript Fetch API接口c++代码实现(二)
javascript·c++·chrome
yanlele8 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范