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

2022-09-26 11:13:42 浏览数 (1)

‍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是没有被重新渲染的。

猜你爱看

0 人点赞