阅读 1941

新版react context实践总结

react context的api解决的问题是祖先元素与子孙元素的通信问题,在日常的实践中,用到context的地方还是很多的,在新版的context出来之前旧版的context存在着一定的缺陷,主要是其实现方式是祖先组件一层一层向下传递的过程,只是不用开发者写传递的代码而已,react会自动传递,示意图如下

因为一层一层传递的设计,先不说性能问题,出现三层或者三层以上的结构,当中间组件出现shouldComponentUpdate(pureComponent)这样的优化时,中间组件返回了false,没有render,那么子组件或者后代组件是拿不到新的context的。

新版的context则是无视像shouldComponentUpdate,purecomponent,react.memo这些对于state或者props进行比较从而不render的这些优化的,并且传递的方式发生了根本性的变化

但是在日常开发中,错误、不正确或者不是最佳的写法,往往导致代码的性能没有尽可能最大化的发挥新版context的优势,so,在此总结几种常见的错误,以及最好的写法实践

首先来看一下一种常见的错误

value属性总是被赋值为新的对象

错误写法

import React, { useState, useContext } from 'react';

const TestContext = React.createContext();

function Children(){
    const context = useContext(TestContext);
    console.log('render children', context);
    return 'children';
}

function Body(){
    console.log('body render');
    return (
        <div>
            body
            <Children />
        </div>
    );
}

function Header(){
    console.log('header render');
    return 'header';
}

function Home() {
    const [content, setContent] = useState('yellow');    const [homeRender, setHomeRender] = useState(1);
    const contextValue = useMemo(() => ({ them: content }), [content]);
    return (
        <div>
            <TestContext.Provider value={{ them: content }}>
                <button onClick={() => setHomeRender(homeRender + 1)}>home render</button>
                <button onClick={() => { setContent('block') ; }}>set context</button>
            home
                <Header />
                <Body />
            </ TestContext.Provider >
        </div>
    );
}
复制代码

执行homerender

执行setContent

这种错误对于熟悉react的小伙伴应该能看出问题所在

   <TestContext.Provider value={{ them: content }}>
复制代码

这样的写法传递value,在home每一次render的情况下都会产生一个新的对象,传递给context,从而导致接收context的组件从新渲染

但是从渲染的结果来看好像没什么问题,children也只渲染了一次

但是我们换一种写法,此时我们想对于children进行优化,我们使用react.memo包一下(这里只展示更改的代码)

const ChildrenMemo = React.memo(Children);
function Body(){
    console.log('body render');
    return (
        <div>
            body
            <ChildrenMemo />
        </div>
    );
}
复制代码

正常情况下来讲,children并没有接收props,所以当父组件渲染时,children是不会触发render的,但是当我们执行homerender的时候

children也跟着渲染了,因为新版api无视react.memo的特性,又因为写法问题(每一次渲染都产生一个新的对象传递给context),导致react.memo失效

优化写法

我们可以使用hook的api,usememo很方便的对于value进行缓存优化

   const contextValue = useMemo(() => ({ them: content }), [content]);
   <TestContext.Provider value={contextValue}>
复制代码

再次测试:

homeRender

setContext

可以看到home render的时候因为react.memo的优化并没有触发children render,而只有context更新的时候children才执行render

但是还记得之前我们说的,新版最大的优势是跳过中间组件,直接传递context么,那么理想的状态不应该是祖先组件context更新,孙组件接收context更新,中间父组件由于没有接收context,而不进行render

跳过中间组件的context

我们直接上代码

import React, { useState, useContext, useMemo } from 'react';

const TestContext = React.createContext();

function Children(){
    const context = useContext(TestContext);
    console.log('render children', context);
    return 'children';
}
const ChildrenMemo = React.memo(Children);
function Body(){
    console.log('body render');
    return (
        <div>
            body
            <ChildrenMemo />
        </div>
    );
}


function Header(){
    console.log('header render');
    return 'header';
}


function TestProvider({ children }) {
    const [content, setContent] = useState('yellow');
    const contextValue = useMemo(() => ({ them: content }), [content]);
    return (
        <>
            <TestContext.Provider value={contextValue}>
                {
                    children
                }
                <button onClick={() => { setContent('block') ; }}>set context</button>
            </ TestContext.Provider >


        </>
    );
}


function Home() {
    const [homeRender, setHomeRender] = useState(1);
    console.log('home render');
    return (
        <div>
            <TestProvider>
                <button onClick={() => setHomeRender(homeRender + 1)}>home render</button>
                    home
                <Header />
                <Body />
            </TestProvider>
        </div>
    );
}
复制代码

我们抽离了context相关的state到一个单独的provider组件中,并使用放在了home中,包裹了header组件和body组件

为了方便测试我们在home中执行了一次log

homerender

children组件并没有重现render,因为我们使用react.memo包裹了children组件

setContext

只有children组件重现渲染,完美

我们分析一下各个组件不渲染和渲染的原因

home组件: provider为home的子组件,子组件state更新不会影响到父组件

header/body: 这两个组件作为props的children属性传递到provider中,并不算provider的子组件,所以并没有render

children: context的更新触发了children的重新render

so,日常我们在开发过程中,注意context provider组件的抽离会对整体的性能产生不小的提升

避免滥用context

最后一点,也是最终的一点是,应该避免滥用context,例如上述的示例代码就是滥用context,context传递的应该是一个不常变的数据,在react-redux6版本就是因为采用的context来直接传递state而导致一些性能下降的问题,react团队也提到了相关问题,他们是不建议使用context传递多变数据的,所以更加合理的组件设计才是性能提升的最基本也是最有效的解决办法.

文章分类
前端
文章标签