1
\$\begingroup\$

This is a contrived example of a question that came up in code review the other day about memoizing context values in React. Let's say I have a hook which returns a memoized value of an expensive operation like so

import { useMemo } from 'react'

const ONE_MILLION = 1_000_000

const ZERO_TO_ONE_MILLION = Array(ONE_MILLION).fill(0).map((_, i) => i)

export type NumberSelector = (n: number) => boolean

/**
 * This hook returns a memoized sequence of numbers from 0 to 1,000,00
 * for which the given selector returns true.
 */
export function useSequence(selector: NumberSelector): number[] {
  return useMemo(() => ZERO_TO_ONE_MILLION.filter(selector), [selector])
}

Then let's say we want a context which contains several sequences

import { createContext, PropsWithChildren } from 'react'
import { selectOddNumber, selectEvenNumber, selectPowerOfTwo } from './selectors'
import { useSequence } from './useSequence'

export type Numbers = {
  odds: number[]
  evens: number[]
  powersOfTwo: number[]
}

export const NumberContext = createContext<Numbers>({
  odds: [],
  evens: [],
  powersOfTwo: [],
})

export function NumberProvider({ children }: PropsWithChildren<{}>) {

  // compute expensive calculations
  const odds = useSequence(selectOddNumber)
  const evens = useSequence(selectEvenNumber)
  const powersOfTwo = useSequence(selectPowerOfTwo)

  return (
    <NumberContext.Provider value={{ odds, evens, powersOfTwo }}>
      {children}
    </NumberContext.Provider>
  )
}

My coworker mentioned in the code-review that I should also memoize the value passed to the provider, something like:

const memoizedValue = useMemo(() => ({ odds, evens, powersOfTwo }), [odds, evens, powersOfTwo])

return (
  <NumberContext.Provider value={memoizedValue}>
    {children}
  </NumberContext.Provider>
)

Otherwise this would cause the sub-tree to re-render unnecessarily. However, I am not so convinced since our application is mainly using functional components which will re-render even if the props stay the same.

My main two questions are:

  • Does React always re-render the subtree unless specified otherwise with memo, etc.
  • Should we always memoized the value to a context provider?

Just for a little more context we primarily interact with a hook

import { useContext } from 'react'
import { NumberContext, Numbers } from './index'

export function useNumbers(): Numbers {
  return useContext(NumberContext)
}

Also here is an example of the code, thanks!

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

So the way React contexts work can be basically boiled down to: if that context re-renders any component which useContext's that context will re-render.

So in this instance, I think that additional useMemo your coworker is suggesting may actually cause another unnecessary additional render though I could be wrong on that point. B/c when one of thos useSequence values changes, your context is rerendering anyways. On that next render, your new useMemo's dependency list would have altered, causing another re-render as it computes the new memoized values which are the same.

useMemo is pretty often misused. Your initial usage inside the hook is perfect though.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.