React Context API并避免重新渲染

And*_*rew 10 javascript reactjs react-redux

我已经更新了底部的更新

有没有办法维护单一的根状态(如Redux),多个Context API使用者在自己的Provider值上工作,而不会在每次孤立的更改中触发重新呈现?

已经阅读了这个相关问题并尝试了一些变体来测试那里提供的一些见解,我仍然对如何避免重新渲染感到困惑.

完整代码如下所示:https://codesandbox.io/s/504qzw02nl

问题在于,根据devtools,每个组件都会看到"更新"(重新渲染),即使它SectionB是唯一看到任何渲染更改的组件,即使它b是状态树中唯一发生变化的部分.我已尝试使用功能组件,PureComponent并看到相同的渲染颠簸.

因为没有任何东西作为道具传递(在组件级别),我无法看到如何检测或阻止这种情况.在这种情况下,我将整个应用程序状态传递给提供程序,但我也尝试传递状态树的片段并看到相同的问题.显然,我做错了.

import React, { Component, createContext } from 'react';

const defaultState = {
    a: { x: 1, y: 2, z: 3 },
    b: { x: 4, y: 5, z: 6 },
    incrementBX: () => { }
};

let Context = createContext(defaultState);

class App extends Component {
    constructor(...args) {
        super(...args);

        this.state = {
            ...defaultState,
            incrementBX: this.incrementBX.bind(this)
        }
    }

    incrementBX() {
        let { b } = this.state;
        let newB = { ...b, x: b.x + 1 };
        this.setState({ b: newB });
    }

    render() {
        return (
            <Context.Provider value={this.state}>
                <SectionA />
                <SectionB />
                <SectionC />
            </Context.Provider>
        );
    }
}

export default App;

class SectionA extends Component {
    render() {
        return (<Context.Consumer>{
            ({ a }) => <div>{a.x}</div>
        }</Context.Consumer>);
    }
}

class SectionB extends Component {
    render() {
        return (<Context.Consumer>{
            ({ b }) => <div>{b.x}</div>
        }</Context.Consumer>);
    }
}

class SectionC extends Component {
    render() {
        return (<Context.Consumer>{
            ({ incrementBX }) => <button onClick={incrementBX}>Increment a x</button>
        }</Context.Consumer>);
    }
}
Run Code Online (Sandbox Code Playgroud)

编辑:我知道react-devtools检测或显示重新渲染的方式可能存在错误.我以一种显示问题的方式扩展了上面的代码.我现在无法判断我正在做什么实际上是否导致重新渲染.根据我从丹·阿布拉莫夫那里读到的内容,我认为我正确地使用了提供者和消费者,但我无法明确地判断这是否属实.我欢迎任何见解.

ped*_*ern 15

有一些方法可以避免重新渲染,也可以使您的状态管理“类似于 redux”。我将向您展示我一直在做的事情,它远非 redux,因为 redux 提供了许多实现起来并不那么简单的功能,例如能够从任何操作或 combineReducers 将操作分派给任何 reducer 等很多其他的。

创建您的减速机

export const initialState = {
  ...
};

export const reducer = (state, action) => {
  ...
};
Run Code Online (Sandbox Code Playgroud)

创建您的 ContextProvider 组件

export const AppContext = React.createContext({someDefaultValue})

export function ContextProvider(props) {

  const [state, dispatch] = useReducer(reducer, initialState)

  const context = {
    someValue: state.someValue,
    someOtherValue: state.someOtherValue,
    setSomeValue: input => dispatch('something'),
  }

  return (
    <AppContext.Provider value={context}>
      {props.children}
    </AppContext.Provider>
  );
}
Run Code Online (Sandbox Code Playgroud)

在应用程序的顶层或您想要的地方使用您的 ContextProvider

function App(props) {
  ...
  return(
    <AppContext>
      ...
    </AppContext>
  )
}
Run Code Online (Sandbox Code Playgroud)

将组件编写为纯函数式组件

这样,它们只会在这些特定依赖项使用新值更新时重新渲染

const MyComponent = React.memo(({
    somePropFromContext,
    setSomePropFromContext,
    otherPropFromContext, 
    someRegularPropNotFromContext,  
}) => {
    ... // regular component logic
    return(
        ... // regular component return
    )
});
Run Code Online (Sandbox Code Playgroud)

具有从上下文中选择道具的功能(如 redux 地图...)

function select(){
  const { someValue, otherValue, setSomeValue } = useContext(AppContext);
  return {
    somePropFromContext: someValue,
    setSomePropFromContext: setSomeValue,
    otherPropFromContext: otherValue,
  }
}
Run Code Online (Sandbox Code Playgroud)

编写一个 connectToContext HOC

function connectToContext(WrappedComponent, select){
  return function(props){
    const selectors = select();
    return <WrappedComponent {...selectors} {...props}/>
  }
}
Run Code Online (Sandbox Code Playgroud)

把它们放在一起

import connectToContext from ...
import AppContext from ...

const MyComponent = React.memo(...
  ...
)

function select(){
  ...
}

export default connectToContext(MyComponent, select)
Run Code Online (Sandbox Code Playgroud)

用法

<MyComponent someRegularPropNotFromContext={something} />

//inside MyComponent:
...
  <button onClick={input => setSomeValueFromContext(input)}>...
...
Run Code Online (Sandbox Code Playgroud)

我在其他 StackOverflow 问题上所做的演示

代码沙盒上的演示

避免了重新渲染

MyComponent仅当上下文中的特定道具使用新值更新时才会重新渲染,否则它将保留在那里。select每次更新上下文中的任何值时,里面的代码都会运行,但它什么也不做,而且很便宜。

其他解决方案

我建议查看使用 React.memo 和 useContext 钩子防止重新渲染。

  • 如果您有复杂且大型的组件,useMemo 可以节省大量计算时间,但对于简单的情况,是的,更容易让重新渲染,而不会提高性能。 (2认同)

Isa*_*aac 1

据我了解,context API 并不是为了避免重新渲染,而是更像 Redux。如果您希望避免重新渲染,也许可以查看PureComponent生命周期钩子shouldComponentUpdate

这是一个提高性能的重要链接,您也可以将其应用于上下文 API

  • 不明白,这个问题怎么回答?React 上下文更改将触发重新渲染,无论“shouldComponentUpdate”如何(如此处所示 https://reactjs.org/docs/context.html#contextprovider) (5认同)
  • 我已经看过“shouldComponentUpdate”,但它似乎是一个死胡同。Provider 的值通过 Consumer 的 HoF 参数传播,而“shouldComponentUpdate”需要访问组件 props。所以我还需要在两个地方都通过它们。阅读了 Dan Abramov 在 Context 上的推文后,我的感觉是我的方法(或我的代码)实际上以一种我看不到的方式被破坏了。 (2认同)