前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >createContext & useContext 上下文 跨组件透传与性能优化篇

createContext & useContext 上下文 跨组件透传与性能优化篇

作者头像
用户2231227
发布2022-09-26 11:13:42
1.7K0
发布2022-09-26 11:13:42
举报
文章被收录于专栏:前端技术专栏-吴佳

‍createContext‍‍‍

createContext api 可以创建一个 React 的 上下文对象,如果使用了这个上下文对象中Provider组件,就可以拿到上下文中提供的数据或者其它信息。

使用方式:

代码语言:javascript
复制
const defaultValue = {}
const MyContext = React.createContext(defaultValue)

如果要使用创建的上下文,需要通过 Context 对象上的 Provider 最外层包装组件,使用方式如下:

代码语言:javascript
复制
<MyContext.Provider value={{ a: 123, b: 222, fn: () => console.log('fn1')))) }}>
    <A>
        <MyContext1.Provider value={{ fn: () => console.log('fn2'))) }}>
            <B>
                <C></C>
            </B>
        </MyContext1.Provider>
    </A>
</MyContext.Provider>

需要通过上面的方式传入value值,指定 Context 要暴露的信息。

子组件在匹配过程中只会匹配最新的 Provider,如果 MyContext 和 MyContext1 提供了相同的方法,则 C 组件只会选择 MyContext1 提供的方法。

默认值的作用?

如果匹配不到最新的 Provider 就会使用默认值,默认值一般只有在对组件进行单元测试(组件并未嵌入到父组件中)的时候比较有用。

‍ 使用useContext获取上下文

通过 createContext 创建出来的上下文对象,在子组件中可以通过 useContext 获取 Provider 提供的内容

代码语言:javascript
复制
const { fn, a, b } = useContext(MyContext);

可以发现useContext 需要将 MyContext 这个 Context 实例传入

这种用法会存在一个比较尴尬的地方,就是父子组件如果不在一个目录中,如何共享 MyContext 这个 Context 实例呢?

一般这种情况下,可以通过 Context Manager 统一管理上下文的实例,然后通过 export 将实例导出,在子组件中将实例 import 进来。

示例:

创建一个contextmanager文件进行统一管理Context创建实例然后将其导出;

代码语言:javascript
复制
import React from 'react';
export const MyContext = React.createContext();
export const MyContext1 = React.createContext();

在需要使用到对应实例的组件中分别去将对应Context实例导入进去使用

代码语言:javascript
复制
import { useContext } from 'react';
import { MyContext1 } from '@/utils/contextmanager'

const Component = () => {
    const { fn } = useContext(MyContext1);
    return <>Component</>
}

‍ createContext和useContext实现数据共享

例子:比如子组件中需要修改父组件的 state 状态

一般的做法是将父组件的方法比如 setXXX 通过 props 的方式传给子组件,而一旦子组件多层级的话,就要层层透传。

如果使用 Context 就可以避免这种层层透传

父组件Provider提供上下文value

代码语言:javascript
复制
import React, { useState } from 'react';
import Child from './Child';
import { MyContext } from '@/utils/contextmanager';

const Parent = (props = {}) => {
    const [step, setStep] = useState(0);
    const [count, setCount] = useState(0);
    const [number, setNumber] = useState(0);

    return (
        <MyContext.Provider value={{ setStep, setCount, setNumber }}>
            <A>
                <Child step={step} number={number} count={count} />
            </A>
        </MyContext.Provider>
    );
}

export default Parent;

子组件 useContext 使用上下文

代码语言:javascript
复制
import React, { useContext, memo } from 'react';
import { MyContext } from '@/utils/contextmanager';

const Child = memo((props = {}) => {
    const { setStep, setNumber, setCount } = useContext(MyContext);

    return (
        <>
            <p>step is : {props.step}</p>
            <p>number is : {props.number}</p>
            <p>count is : {props.count}</p>
            <hr />
            <div>
                <button onClick={() => { setStep(props.step + 1) }}>step ++</button>
                <button onClick={() => { setNumber(props.number + 1) }}>number ++</button>
                <button onClick={() => { setCount(props.step + props.number) }}>number + step</button>
            </div>
        </>
    );
});

export default Child;

效果

关于使用memo是为了说明这个写法在这里是多余的

memo的作用是为了减少组件重新render过程中导致组件的重复渲染,而得到性能上的提升。

但是 Context 发生的变化是无法通过 memo 进行优化的,这个大家需要知道的一个点。

‍ 使用useReducer优化Context复杂度

上面的示例虽然实现了多级组件方法共享,但是暴露出一个问题。

就是把所有的方法都放在了 MyContext.Provider.value 属性中传递,必然造成整个 Context Provider 提供的方法越来越多,让维护变的就更复杂了。

这里其实可以通过useReducer包装,通过dispatch去触发action进行数据更新,所以我们可以如下优化一下上面代码

父组件

代码语言:javascript
复制
import React, { useReducer } from 'react';
import Child from './Child';
import { MyContext } from '@/utils/contextmanager';

const initState = { count: 0, step: 0, number: 0 };

const reducer = (state, action) => {
    switch (action.type) {
        case 'step': return Object.assign({}, state, { step: state.step + 1 });
        case 'number': return Object.assign({}, state, { number: state.number + 1 });
        case 'count': return Object.assign({}, state, { count: state.step + state.number });
        default: return state;
    }
}

const Parent = (props = {}) => {
    const [state: { step, number, count }, dispatch] = useReducer(reducer, initState);

    return (
        <MyContext.Provider value={{ dispatch }}>
            <Child step={step} number={number} count={count} />
        </MyContext.Provider>
    );
}

export default Parent;

子组件

代码语言:javascript
复制
import React, { useContext, memo } from 'react';
import { MyContext } from '@/utils/contextmanager';

const Child = memo((props = {}) => {
    const { dispatch } = useContext(MyContext);

    return (
        <>
            <p>step is : {props.step}</p>
            <p>number is : {props.number}</p>
            <p>count is : {props.count}</p>
            <hr />
            <div>
                <button onClick={() => { dispatch({ type: 'setp' }) }}>step ++</button>
                <button onClick={() => { dispatch({ type: 'number' }) }}>number ++</button>
                <button onClick={() => { dispatch({ type: 'count' }) }}>number + step</button>
            </div>
        </>
    );
});

export default Child;

‍ 将state也通过Context传递给子组件

其实上面的例子,子组件获取父组件的 state 还是通过 props传递的,其实也会存在层层嵌套

如果将整个 state 通过 Context 传入就无需层层组件的 props 传递(如果不需要整个state,可以只将某几个 state 给 Provider)

优化后,父组件

代码语言:javascript
复制
import React, { useReducer, useCallback } from 'react';
import Child from './Child';
import { MyContext } from '@/utils/contextmanager';

const initState = { count: 0, step: 0, number: 0 };

const reducer = (state, action) => {
    switch (action.type) {
        case 'step': return Object.assign({}, state, { step: state.step + 1 });
        case 'number': return Object.assign({}, state, { number: state.number + 1 });
        case 'count': return Object.assign({}, state, { count: state.step + state.number });
        default: return state;
    }
}

const Parent = (props = {}) => {
    const [state, dispatch] = useReducer(reducer, initState);
    const parentStepHandler = useCallback(() => {
       dispatch({ type: 'step' })     
    }, [])

    return (
        <MyContext.Provider value={{ dispatch, state }}>
            <button onClick={parentStepHandler}>parent step ++</button>
            <Child />
        </MyContext.Provider>
    );
}

export default Parent;

优化后,子组件

代码语言:javascript
复制
import React, { useContext, memo } from 'react';
import { MyContext } from '@/utils/contextmanager';

const Child = memo((props = {}) => {
    const { state: { step, number, count }, dispatch } = useContext(MyContext);

    return (
        <>
            {console.log('[Child] RELOAD-RENDER')}
            <p>step is : { step }</p>
            <p>number is : { number }</p>
            <p>count is : { count }</p>
            <hr />
            <div>
                <button onClick={() => { dispatch({ type: 'setp' }) }}>step ++</button>
                <button onClick={() => { dispatch({ type: 'number' }) }}>number ++</button>
                <button onClick={() => { dispatch({ type: 'count' }) }}>number + step</button>
            </div>
        </>
    );
});

export default Child;

效果

直接使用父组件 state 的性能问题

注意看上面的动图,在点击子组件的 【number + step】 按钮的时候,虽然 count 的值没有发生任何变化,但是一直触发[Child] RELOAD-RENDER 的打印,即使子组件是通过 memo 包装过的。

出现这个问题原因是 memo 只会对 props 进行浅比较,而我们直接将 state 注入到了组件内部,因此 state 的变化必然会触发[Child] RELOAD-RENDER,整个 state 变化是绕过了 memo。

‍ 使用useMemo方式来解决上面state透传性能问题

使用 useMemo 优化子组件渲染

代码语言:javascript
复制
import React, { useContext, useMemo } from 'react';
import { MyContext } from '@/utils/contextmanager';

const Child = (props = {}) => {
    const { state: { step, number, count }, dispatch } = useContext(MyContext);

    const Content = useMemo(() => {
        return <>
            {console.log('[Child] RELOAD-RENDER')}
            <p>step is : { step }</p>
            <p>number is : { number }</p>
            <p>count is : { count }</p>
            <hr />
            <div>
                <button onClick={() => { dispatch({ type: 'setp' }) }}>step ++</button>
                <button onClick={() => { dispatch({ type: 'number' }) }}>number ++</button>
                <button onClick={() => { dispatch({ type: 'count' }) }}>number + step</button>
            </div>
        </>    
    }, [step, number, count, dispatch]);

    return Content;
};

export default Child;

效果

通过上面效果可以看到,点击 number + step 按钮不变的时候是不会再触发打印的,所以DOM是没有被重新渲染的。

猜你爱看

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-08-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端技术专栏 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档