React Context API and avoiding re-renders

There are some ways to avoid re-renders, also make your state management "redux-like". I will show you how I've been doing, it far from being a redux, because redux offer so many functionalities that aren't so trivial to implement, like the ability to dispatch actions to any reducer from any actions or the combineReducers and so many others.

Create your reducer

export const initialState = {
  ...
};

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

Create your ContextProvider component

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>
  );
}

Use your ContextProvider at top level of your App, or where you want it

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

Write components as pure functional component

This way they will only re-render when those specific dependencies update with new values

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

Have a function to select props from context (like redux map...)

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

Write a connectToContext HOC

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

Put it all together

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

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

function select(){
  ...
}

export default connectToContext(MyComponent, select)

Usage

<MyComponent someRegularPropNotFromContext={something} />

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

Demo that I did on other StackOverflow question

Demo on codesandbox

The re-render avoided

MyComponent will re-render only if the specifics props from context updates with a new value, else it will stay there. The code inside select will run every time any value from context updates, but it does nothing and is cheap.

Other solutions

I suggest check this out Preventing rerenders with React.memo and useContext hook.


To my understanding, the context API is not meant to avoid re-render but is more like Redux. If you wish to avoid re-render, perhaps looks into PureComponent or lifecycle hook shouldComponentUpdate.

Here is a great link to improve performance, you can apply the same to the context API too


I made a proof of concept on how to benefit from React.Context, but avoid re-rendering children that consume the context object. The solution makes use of React.useRef and CustomEvent. Whenever you change count or lang, only the component consuming the specific proprety gets updated.

Check it out below, or try the 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}