2
\$\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

2
\$\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\$

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.