React: How I learned to create optimized contexts

… and stop worrying about the bomb.

Thomas Juster
Level Up Coding

--

Why does everyone keep talking about contexts … ?

A bit of … well, context 🤭

Note: skip this section if you wanna dive straight away.

React contexts is a very cool feature, and extremely practical to use with the hooks API. You’ve surely met some, and probably recognize them right way.

Today I’d like to share what I learned when creating my own React contexts: what I consider to be a good context and how to write one.

There’s another exploration I’d like to share later that won’t be in this post: how and when to split a context in 2 or more.

Let’s dive

Le awesome dive

Let’s take the example of a Theme context, providing a mode: 'dark' or 'light', and a compact boolean to indicate whether the UI should have reduced margins and paddings or not.

First approach

Basically, I’d create a context using React createContext and just export it, that would do it.

And you would consume this context using the useContext hook or the consumer component like so:

const theme = useContext(ThemeContext)
// or
<ThemeContext.Consumer>
{(theme) => …}
</ThemeContext.Consumer>

Fine, but this is quite unsafe.

Did you notice how the fallback value provides no-op functions for setMode and setCompact? That’s because we can’t be sure a provider was declared upper in the component tree.

Plus, which component will be in charge of providing the value ?

What I consider to be a good context

To me, a good context should:

  • Be easily maintainable (you usually don’t work alone, do you ? Even if you do, you work with your past and future self 🤐)
  • Fail fast, in case someone forgot to provide a value. Failing fast prevents silent errors, which are horribly hard to find.
  • Embed its own logic − don’t leave that logic to a component not responsible for it.
  • Make its consumers re-render only when necessary

How to write a good context

Architecture/Skeleton 🏚

Beforehand note: What follows is opinionated, and definitely not the only viable way of writing React contexts. I’d be glad to hear about yours too.

We won’t export the − unsafe − context directly. Instead, we will export 3 wrappers to secure it:

  • A Provider component − <ThemeProvider /> −, it will embed its own logic instead of delegating it god-knows-where.
  • A hook, to consume the context value − useTheme() −, it will assert that the context has been provided a value.
  • If needed: a Consumer component − <ThemeConsumer /> − same as the hook, it will assert that the context has been provided a value.

Consumers: asserting the provided value

Mmh, nope.

To assert the context value, we will add an arbitrary fallback value − null here, but it could be any arbitrary value −, and the consumers useTheme and ThemeConsumer will assert that the context value is not the fallback value.

Contol-freak consumers

Providers: avoid unnecessary re-renders

Hold on, this is the last mile 🥵. Here’s the basic −yet inefficient − implementation:

Inefficient provider being inefficient.

“Why is it inefficient” you’d ask ? Glad you asked: Because of the value !

When the parent of ThemeProvider re-renders, ThemeProvider will re-render too, which will recreate the value, thus changing its reference.
Then, React will detect a context value change, and every component consuming our Theme will update !
And by the way, keep in mind that this applies to any JavaScript object: Array, Map, Set, etc… (I made a codesandbox to demonstrate that glitch).

To tackle that, we should memoize the value with the useMemo hook to prevent reference changes, and we’re good to go.

Note 1: Most of the contexts will appear at the root of your app⋅s, meaning they won’t have any parent that might re-render. Still, when writing a context provider, the provider cannot know in advance where in the tree it will be used. Because of that, you should always optimize your providers.

Note 2: During my experiment I was advised to wrap the provider component in a React.memo HOC. That was a good idea, unfortunately it doesn’t work. So don’t. Try it yourself here (codesandbox).

Note 3: Instead of useMemo, you could also use a grouped state like so:
const [theme, setTheme] = useState({ mode: 'dark', compact: false })

Note 4: There’s also the possibility of using the useReducer hook to manage state, but I feel like this hook is quasi-only understood by redux-users, so I’m not comfortable using it in group projects.

Wrapping up 🧐

Your next dive with a React Context 🤓
  • The architecture Provider + hook (+ Consumer) ensures readability (thus maintainability) and embeds the logic at one place, diminishing our error sources.
  • The hook (and the Consumer component) implement a fail-fast strategy, to avoid silent errors and desperate debugging sessions.
  • The Provider component is optimized to avoid unnecessary re-renders of its consumers.
Final glance 💅

That’s it ! I hope it will help you on your context journeys, now code safe and see you around 😇.

FYI: There’s a codesandbox here and at the end if you’d like to play around with contexts 🤪.

So see you next tim…, oh. wait. I think we can do more 😏.

Going further 😤

🙇 🙇‍♂️ 🙇‍♀️

You didn’t see this one coming huh ? There are two repetitive logics we can abstract:

  • Creating contexts with a unique and identifiable default value.
  • Creating consumers − hook and component − that throw when the default value is found.

For the default value, since the null value might be used in a context some day (and since we don’t know what the future holds 🔮), we have to find another uniquely identifiable default value. And JavaScript has the perfect data type for that: Symbol.

Editor’s note: I’m like a child right now… I know it’s a weird dream, but I seriously dreamt of using a JavaScript Symbol with an appropriate use case one day… and this day has come ! 🥳 🤓

Now I’m done. Really. Not joking.

Here, take that GIF as a reward for going that far 😎

Congrats !
You, next time a context bothers you.

The promised codesandbox 😊

🦄

--

--