7

React Context Module Pattern

 2 years ago
source link: https://blog.variant.no/react-context-module-pattern-63fcd9aacd0d
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

React Context Module Pattern

I’ve always liked programming patterns. Not as a dogmatic “always preemptively follow this”, but as “wow, this is a way to structure code that has worked in some contexts, and that I can learn from”. Patterns are a good way to reflect on battle-tested experiences abstracted as reusable guides. This post is such a thing, albeit a very simple one. A summary of how we as a team have organized React Contexts for maintainability, encapsulation, and consumption.

1*SFQSO9xWZjFhhPNfQFrG5w.jpeg

As an example case, we will be building a context for handling alerts or toasts on a site. It is simple, yet has some state and automatic actions to go with it. As such it is non-trivial. The overall goal of the context module is to have a self-contained usable hook with a shared state that is intuitive to use with an ergonomic API.

I tend to overexplain and go into too many details. If you just want to skip to a complete example with comments on the different parts of how to use React context module, see this codesandbox also this VSCode snippet. Anyways, let’s get started.

For context: The 3 parts of contexts

Just a quick reminder on context, and we’ll build from there. To use context we need 3 different parts: The provider populating a context with values, the consumer allowing for access to said value, and the state which holds the value. A straightforward approach could yield something like this when following the docs:

And this straightforward way to solve it does the job in many cases! However, it has some shortcomings:

  • It has little control over undefined and defined values for our API consumers,
  • it lacks named concepts which makes it harder to understand and remember,
  • it’s spread around all over making it hard to see what is connected and not,
  • and most of all, expanding the state and evolving the code is difficult.

Let’s handle this one by one, starting with the most important one.

1. Allowing for extensions and change

We can allow for extending the context value a lot by just taking one small step: Wrap state in an object:

Although, a small change, we’ve now gone from something very hard to extend to very easy. If we want to extend the values in our context we can do that as a backward-compatible change that doesn’t ruin anything for all potential consumers.

At a different place in our code, we can utilize the new addAlert, without affecting the other components.

We have a potential problem here, though. If we want to use addAlert as a dependency to a useEffect here, we get into trouble:

1.1 Return stable functions from contexts

Since the addAlert we pass into the context changes at every render, it isn't what I like to call stable. I like to keep every value we return from contexts stable. alerts is already stable as it is in useState, but addAlert is generated at every rerender. We can change that using useCallback.

Although useCallback can have some performance impact and it's often overused, I think that having functions returned from contexts stable is well worth the trade-offs. Every value returned in a context should be only changed, and have referential changes when the actual value is changed. Until JavaScript has proper value types, we'll make due by memoizing and caching references using things like useMemo and useCallback.

1.2 No-op defaults

If you’ve tried merging all code and making it run, you’ll also see another issue we have. The value we pass to our context doesn’t match our specified context state type.

Fine, so we’ll add our addAlert function to the type definition:

All good, no? Ah, no. Our initial state no longer matches the state:

To fix the typing issue we need to add an addAlert implementation that matches the signature. But our addAlert requires interacting with the state. How can we add a function we don't have access to? We'll just add a substitute function that has no-operations. We could specify by the state that addAlert can be undefined, but that will propagate issues every time we want to access it we need to check for existence. And I don't know about you but an existential crisis can be demanding. That's why I always try to establish if something exists as soon as possible, and most of the time I try to find a way to avoid non-existing things.

To provide a sane default we can do something like:

We’ll look more into different ways to handle default values in part 4. active defaults.

2. Collecting it all as a module

From our example now we have sprinkled our code with some createContext here, some types there, and some logic here and there. It's hard to decipher what is relevant for using alerts, what is an implementation detail, and what is just helper code. We need to encapsulate our code and isolate it better.

Let's start by creating a new file and moving code that is relevant to our context.

We have a problem here though. All the hairiest content of our context is still in our App component. That makes it hard to maintain and separate what is needed in App and what is needed by the Alert Context. We need to find a way to move it to alert-context.ts. The simplest step might be to create a custom hook like this:

And this works just fine! But I think this is kind of an abstraction leakage. We export two bits of information that are immediately sewn back together again when using them. Not only is it unnecessary, but it is more error-prone and not intuitive. We can encapsulate more by not letting our consumers use the AlertContext.Provider directly:

Now we’ve simplified our App component, and don't allow our API users to do any mistakes while sewing:

2.1 Passing data and initials

But how would we now start our application with an annoying alert when opening it? By passing props as normal:

2.2 Using hidden context

In our MyComponent we still use it as before:

But useContext(AlertContext) isn't the cleanest API. As we know from any wizardry, naming things has power. This is also true for programming.

3. The power of naming things

Naming things correctly and helpfully has enormous power for ease of coding and maintainability. We always have to remember that programming is communication. If not to others, then to ourselves in the future — which is always the toughest crowd. So don’t make our future selfs disappointed: let’s find a better name for useContext(AlertContex) and make it more explicit. What would be a better name? How about something as simple as useAlert? But how you ask? Functions of course.

and with that we can do:

Which is slightly better in my opinion. Speaking of naming things: Rumpelstiltskin. Also, in our now increasing alert-contex.tsx we have been very particular in how we name things. It's following a specific pattern that is made to make it consistent:

This is the naming pattern and template I follow all the time. As a bonus, here is a VS Code snippet you can use for theTypeScript React language.

But say, earlier I said “exporting two things and sewing them back together” is a negative thing. But here we still do that with, in our case, AlertContextProvider and useAlert. Let's look more into it in the next section.

4. Active and inactive defaults

To use addAlert now, with any form of interactivity and function we have to do:

And as MyComponent is within AlertContextProvider we're able to properly list or add alerts. But what would happen to useAlert if our App was like this:

Now useContext(AlertContext) (which useAlert is an alias for) will return our default initial state:

In other words. It wouldn’t do anything at all. Just a static empty list. Which in some cases is valid. There are use cases where you want your hook accessor to your context to work no matter what. Let’s say you did a translation module. In those cases, you might want to have it return some default text no matter if it is wrapped in a provider or not. However, I’ve found that in most cases, I always want to make sure my accessor hook is invoked as a descendent of my provider.

Since we’ve extracted useAlert as our custom hook, we can do that change quickly:

This will cause a runtime error if useAlert is ever used outside of AlertContextProvider. Yes, it is a runtime error, and it would be better with a build error. But honestly, this is something that would explode fairly quickly when actually using it and verifying your code afterward. Over 2 years of using this pattern I've yet to push any code that crashes in production due to this error.

In summary

In this post, we’ve gone through what I believe to be a very common pattern for using React contexts. But by transitioning from using React contexts from the React docs over to this pattern, we’ve looked into the motivation behind how to structure it and what advantages it brings. In the end, we have a context that could look something like this (full example). You can also see the full running example on codesandbox.io.

Complete example


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK