The way that context works is that whenever the provided value changes from one render to another, it triggers a re-render of all the consuming components (which will re-render whether or not they’re memoized).
So take this for example:
type CountContextValue = readonly [number, Dispatch<SetStateAction<number>>]const CountContext = createContext<CountContextValue | null>(null)function CountProvider(props) {const [count, setCount] = useState(0)const value = [count, setCount]return <CountContext value={value} {...props} />}
Every time the <CountProvider />
is re-rendered, the value
is brand new, so
even though the count
value itself may stay the same, all component consumers
will be re-rendered.
This can be problematic in certain scenarios.
The quick and easy solution to this problem is to memoize the value that you provide to the context provider:
type CountContextValue = readonly [number, Dispatch<SetStateAction<number>>]const CountContext = createContext<CountContextValue | null>(null)function CountProvider(props) {const [count, setCount] = useState(0)const value = useMemo(() => [count, setCount], [count])return <CountContext value={value} {...props} />}
By memoizing the value, you’re ensuring that the value is only re-created when
the count
value changes. As a result, the consuming components will only
rerender when the count
value changes.
🧝♂️ I’ve taken the name
and color
state and combined them into a single
FooterContext
as well as extracted some of the UI into a FooterSetters
component (for all the stuff that allows you to control the Footer
). Feel free
to
👨💼 Great, now if you pull up the React DevTools, you’ll notice all the
components rerender whenever you change the counter in the App component. This
is happening because the FooterContext
is a new value
prop every
render.
So if you memoize the value
prop with useMemo
, you can prevent the
unnecessary rerender of consuming components when the value
prop doesn’t
change.
👨💼 We’ve noticed that when we change some of the Footer context stuff, we’re
actually rerendering the App
and Main
components even though those don’t
have anything to do with the Footer context.
So we’re going to take advantage of React’s optimizations for reusing elements
by creating a FooterProvider
component and accepting children
. That way
whenever the Footer context changes, only the FooterProvider
component and its
consumers will rerender. React will be able to reuse the children
element
since that doesn’t change when the Footer’s state changes.
When you’re finished, changing the footer color of setting the footer name
should not cause the App
or Main
components to rerender.
👨💼 Something you may have noticed is that the FooterSetters
component is
rerendering whenever the footer state changes, but that component doesn’t
actually depend on the footer state at all. All it cares about is the setter
functions which never change!
Let’s assume that FooterSetters
is an expensive component to render. How could
we prevent it from rerendering unnecessarily when the footer state changes?
How about you split the context into two separate contexts: one for the state and one for the setters. For example:
function SomeProvider() {const [state, setState] = useState()const setters = useMemo(() => ({ setState }), [setState])const stateValue = useMemo(() => ({ state }), [state])return (<StateContext value={stateValue}><SettersContext value={setters}>{children}</SettersContext></StateContext>)}
This way, FooterSetters
can consume only the setters
(which never change)!
Give that a shot in this exercise (and for extra credit you can also memo
-ize
the FooterSetters
component and it will never rerender!).
This is going to require a fair bit of refactoring, but it should be pretty quick. Make sure you check out how components rerender in the React DevTools!
import { createContext, memo, use, useMemo, useState } from 'react'import * as ReactDOM from 'react-dom/client'const FooterStateContext = createContext<{color: stringname: string} | null>(null)FooterStateContext.displayName = 'FooterStateContext'const FooterDispatchContext = createContext<{setColor: (color: string) => voidsetName: (name: string) => void} | null>({setColor: () => {},setName: () => {},})FooterDispatchContext.displayName = 'FooterDispatchContext'function FooterProvider({ children }: { children: React.ReactNode }) {const [color, setColor] = useState('black')const [name, setName] = useState('')const footerStateValue = useMemo(() => ({ color, name }), [color, name])const footerDispatchValue = useMemo(() => ({ setColor, setName }), [])return (<FooterStateContext value={footerStateValue}><FooterDispatchContext value={footerDispatchValue}>{children}</FooterDispatchContext></FooterStateContext>)}function useFooterState() {const context = use(FooterStateContext)if (!context) throw new Error('FooterStateContext not found')return context}function useFooterDispatch() {const context = use(FooterDispatchContext)if (!context) throw new Error('FooterDispatchContext not found')return context}const Footer = memo(function FooterImpl() {const { color, name } = useFooterState()return (<footer style={{ color }}>I am the ({color}) footer, {name || 'Unnamed'}</footer>)})function Main({ footer }: { footer: React.ReactNode }) {const [count, setCount] = useState(0)const increment = () => setCount((c) => c + 1)return (<div><button onClick={increment}>The count is {count}</button>{footer}</div>)}const FooterSetters = memo(function FooterImplSetters() {const { setColor, setName } = useFooterDispatch()return (<><div><p>Set the footer color:</p><div style={{ display: 'flex', gap: 4 }}><button onClick={() => setColor('black')}>Black</button><button onClick={() => setColor('blue')}>Blue</button><button onClick={() => setColor('green')}>Green</button></div></div><div><p>Set the footer name:</p><label>Name:<input onChange={(e) => setName(e.currentTarget.value)} /></label></div></>)})function App() {const [appCount, setAppCount] = useState(0)return (<FooterProvider><div><FooterSetters /><button onClick={() => setAppCount((c) => c + 1)}>The app count is {appCount}</button><Main footer={<Footer />} /></div></FooterProvider>)}const rootEl = document.createElement('div')document.body.append(rootEl)ReactDOM.createRoot(rootEl).render(<App />)
Quick Links
Legal Stuff
Social Media