Rect Context with Selectors

When context updates, every component that subscribes to it updates. It is by design. Soon after you learn this you start to look for solutions to prevent unnecessary re-renders.

Martin Adamko
4 min readMar 13, 2023
Example of React Context that re-renders all components that subscribe to it.

Even though a component might need to react only when a specific part of the context changes, there is no escape hatch to it. The useContext() hook runs and returns the context value on every value change. And so the component that subscribes to it needs to re-render.

☝️ HEADS UP!
You can use memo to stop children from being re-rendered but that won’t prevent the componetn consuming context to re-render.

But is it possible to use React Context without the unnecessary re-renders?

And, moreover, is it possible also to:

  • stop the endless nesting of single value Context Providers?
  • “subscribe” only to some of the updates of the parent context?
  • access the context value?
Example of React Context that re-renders only the components that subscribe to the slice of context that got updated.

It is. However…

Most of the solutions opt-out from the default React Context updates altogether and provide their own subscribe/unsubscribe mechanisms to some sort of event based store updates.

👉 TIP: See how e.g. Zustand allows you to use selectors with their store.

…but there is a React way to do that.

Solution: React Context with Selectors

Selectors are functions that take the current state of the context and return a specific piece of data. They are similar to mapStateToProps in Redux, but they are not tied to Redux.

We are basically looking for a solution that checks few conditions:

  1. Must be a Context so we can access it from descendant components;
  2. Must let us select what part of it we are interested in;
  3. Must let us compare when the value changed (e.g. same value but new reference).

Here’s a sketch of possible implementation to give you a rough idea:

// The code below is just for illustration purposes:
import { createContext, useContext, useEffect, useMemo, useRef } from 'react'

type Selector<T, R> = (state: T) => R
type Callback<R> = (result: R) => void
type Subscriptions<T> = Map<Callback<unknown>, Selector<T, unknown>>

function createSelectorContext<T>(context: Context<T>) {
// Step 1: Create a new Context that will be stable
const SubscriptionsContext = createContext<{
subscribe: <R>(selector: Selector<T, R>, callback: Callback<R>) => void;
unsubscribe: (callback: Callback<unknown>) => void;
} | null>(null)

// Step 2: Create a component we will use to pass store value to
const Provider = memo(({ children, value }: { children: React.ReactNode, value: T }) => {
// Step 3: We will manage subscriptions (selectors and callback pairs)
const subscriptions = useRef<Subscriptions<T>>(new Map())

// Step 4: We provide new context value-an API to subscribe/unsubscribe
// to store updates with an selector and callback
const api = useMemo(() => ({
subscribe: <R>(selector: Selector<T, R>, callback: Callback<R>) => {
subscriptions.current.set(callback, selector)
},
unsubscribe: (callback: Callback<unknown>) => {
subscriptions.current.delete(callback)
},
}), [])

// Step 5: We will loop selectors and call callback with the store slice
useEffect(() => {
subscriptions.current.forEach(
(selector, callback) => {
callback(selector(value))
},
)
}, [value])

return (
<SubscriptionsContext.Provider value={api}>
{children}
</SubscriptionsContext.Provider>
)
})

// Step 6: Return wrappers
return [Provider, SubscriptionsContext]
}

Such implementation is still a little cumbersome to use but it should give you the impression of how it could work.

To further simplify usage we can return also Consumer or a hook (do not forget to unsubscribe on unmount after subscribing).

What’s next

☝️ IMPORTANT
Make sure your selectors return stable reference (e.g. numbers, booleans, strings are safe, but objects are recreated on each context update.) Either pass immutable value to the Selector Context Provider that does keeps references of the nested values or add the 2ⁿᵈ argument to the hook to check for equality.

I haven’t saw such implementation anywhere so it might as well be all wrong. It might also be great, it si too new to tell.

I at least hope to inspire you to write your own solutions, better ones even. So please let me know how this works for you!

Please open an issue or reach out to me if you find something is not working as expected.

Cheers! 👋

Martin

--

--

Martin Adamko
Martin Adamko

Written by Martin Adamko

One that loves design, illustration, photography, digs in code, adored his dog and enjoys life & good coffee. http://be.net/martin_adamko

No responses yet