React: How I learned to create optimized contexts
… and stop worrying about the bomb.
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
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
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.
Providers: avoid unnecessary re-renders
Hold on, this is the last mile 🥵. Here’s the basic −yet inefficient − implementation:
“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 🧐
- 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.
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 ! 🥳 🤓
Here, take that GIF as a reward for going that far 😎
The promised codesandbox 😊
Level Up Coding
Thanks for being a part of our community! Subscribe to our YouTube channel or join the Skilled.dev coding interview course.