React Context API并避免重新渲染

时间:2018-07-13 03:49:35

标签: javascript reactjs react-redux

我已经在底部进行了更新

是否可以通过多个Context API使用者维护自己的Provider值的整体根状态(如Redux),而不会在每个孤立的更改上触发重新呈现?

已经read through this related question并尝试了一些变体来测试那里提供的一些见解,但我仍然对如何避免重新渲染感到困惑。

完整的代码在下面,并且在此处在线: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>);
    }
}

编辑:我了解可能有a bug in the way react-devtools条检测到或显示了重新渲染。我以显示问题的方式expanded on my code above。现在,我无法确定我在做什么实际上引起了重新渲染。根据我从Dan Abramov那里读到的内容,我认为我正在正确使用提供者和消费者,但是我不能确切地说出这是否正确。我欢迎有任何见识。

3 个答案:

答案 0 :(得分:1)

据我了解,上下文API并非旨在避免重新渲染,而更像是Redux。如果您希望避免重新渲染,则可以考虑使用PureComponent或生命周期挂钩shouldComponentUpdate

这是提高性能的好方法link,您也可以将其应用于上下文API

答案 1 :(得分:1)

我做了一个关于如何从 React.Context 中受益的概念证明,但避免重新渲染使用上下文对象的子项。该解决方案使用了 React.useRefCustomEvent。每当您更改 countlang 时,只会更新使用特定属性的组件。

在下面查看,或尝试CodeSandbox

index.tsx

import * as React from 'react'
import {render} from 'react-dom'
import {CountProvider, useDispatch, useState} from './count-context'

function useConsume(prop: 'lang' | 'count') {
  const contextState = useState()
  const [state, setState] = React.useState(contextState[prop])

  const listener = (e: CustomEvent) => {
    if (e.detail && prop in e.detail) {
      setState(e.detail[prop])
    }
  }

  React.useEffect(() => {
    document.addEventListener('update', listener)
    return () => {
      document.removeEventListener('update', listener)
    }
  }, [state])

  return state
}

function CountDisplay() {
  const count = useConsume('count')
  console.log('CountDisplay()', count)

  return (
    <div>
      {`The current count is ${count}`}
      <br />
    </div>
  )
}

function LangDisplay() {
  const lang = useConsume('lang')

  console.log('LangDisplay()', lang)

  return <div>{`The lang count is ${lang}`}</div>
}

function Counter() {
  const dispatch = useDispatch()
  return (
    <button onClick={() => dispatch({type: 'increment'})}>
      Increment count
    </button>
  )
}

function ChangeLang() {
  const dispatch = useDispatch()
  return <button onClick={() => dispatch({type: 'switch'})}>Switch</button>
}

function App() {
  return (
    <CountProvider>
      <CountDisplay />
      <LangDisplay />
      <Counter />
      <ChangeLang />
    </CountProvider>
  )
}

const rootElement = document.getElementById('root')
render(<App />, rootElement)

count-context.tsx

import * as React from 'react'

type Action = {type: 'increment'} | {type: 'decrement'} | {type: 'switch'}
type Dispatch = (action: Action) => void
type State = {count: number; lang: string}
type CountProviderProps = {children: React.ReactNode}

const CountStateContext = React.createContext<State | undefined>(undefined)

const CountDispatchContext = React.createContext<Dispatch | undefined>(
  undefined,
)

function countReducer(state: State, action: Action) {
  switch (action.type) {
    case 'increment': {
      return {...state, count: state.count + 1}
    }
    case 'switch': {
      return {...state, lang: state.lang === 'en' ? 'ro' : 'en'}
    }
    default: {
      throw new Error(`Unhandled action type: ${action.type}`)
    }
  }
}

function CountProvider({children}: CountProviderProps) {
  const [state, dispatch] = React.useReducer(countReducer, {
    count: 0,
    lang: 'en',
  })
  const stateRef = React.useRef(state)

  React.useEffect(() => {
    const customEvent = new CustomEvent('update', {
      detail: {count: state.count},
    })
    document.dispatchEvent(customEvent)
  }, [state.count])

  React.useEffect(() => {
    const customEvent = new CustomEvent('update', {
      detail: {lang: state.lang},
    })
    document.dispatchEvent(customEvent)
  }, [state.lang])

  return (
    <CountStateContext.Provider value={stateRef.current}>
      <CountDispatchContext.Provider value={dispatch}>
        {children}
      </CountDispatchContext.Provider>
    </CountStateContext.Provider>
  )
}

function useState() {
  const context = React.useContext(CountStateContext)
  if (context === undefined) {
    throw new Error('useCount must be used within a CountProvider')
  }
  return context
}

function useDispatch() {
  const context = React.useContext(CountDispatchContext)
  if (context === undefined) {
    throw new Error('useDispatch must be used within a AccountProvider')
  }
  return context
}

export {CountProvider, useState, useDispatch}

答案 2 :(得分:0)

有一些方法可以避免重新渲染,也可以使您的状态管理“像redux一样”。我将向您展示我的工作方式,它远不是redux,因为redux提供了许多功能,这些功能并不是那么容易实现的,例如能够从任何动作或CombineReducers向任何reducer分配动作等功能。还有很多。

创建您的减速器

export const initialState = {
  ...
};

export const reducer = (state, action) => {
  ...
};

创建您的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>
  );
}

在应用程序的顶级或所需位置使用ContextProvider

function App(props) {
  ...
  return(
    <AppContext>
      ...
    </AppContext>
  )
}

将组件写为纯功能组件

这样,仅当这些特定的依赖项更新为新值时,它们才会重新渲染

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

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

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

写一个connectToContext HOC

function connectToContext(WrappedComponent, select){
  return function(props){
    const selectors = select();
    return <WrappedComponent {...selectors} {...props}/>
  }
}

将它们放在一起

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

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

function select(){
  ...
}

export default connectToContext(MyComponent, select)

用法

<MyComponent someRegularPropNotFromContext={something} />

//inside MyComponent:
...
  <button onClick={input => setSomeValueFromContext(input)}>...
...

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

Demo on codesandbox

避免了重新渲染

MyComponent仅在上下文道具以新值更新时重新渲染,否则它将停留在该位置。 select中的代码将在每次上下文更新中的任何值运行时运行,但是它什么都不做而且很便宜。

其他解决方案

我建议您查看Preventing rerenders with React.memo and useContext hook.