Typesafe Global State with TypeScript, React & React Context

Image for post
Image for post

Whilst working at TfL, I was one of the lead web developers involved in rewriting the their online forms (i.e. Lost Property, Ideas, Font Requests etc.) from an ASP.NET site to a decoupled TypeScript React & .NET Core solution. The Accessibility Guides web form was the pilot form for this rewrite, outlining a patterned structure for rewriting other TfL online forms.

Image for post
Image for post
Giving a facelift to the accessibility guides form — TfL ©

An important technical design decision that needed to be made early on was which, of the multiple state management systems, would we use to store state for this web form. Redux? MobX? Flux? React Context? Truth be told, RecoilJS had just launched and I was pretty hyped to try it out on a production application — alas, it probably would not have been pertinent to use an experimental state utility on a site that would serve 10,000+ users/month!

In this case, perhaps the de facto choice for state management would have been Redux due to its ubiquity in the React world; however even with Redux Toolkit, the learning curve isn’t easy for users from a non-JS / TS background.

As a predominantly OOP C# shop, MobX could have been another clear choice because of similarities with class based structures. Although less of a learning curve, still not as well documented as Redux or Context.

The library I ended up pushing for amongst my colleagues was React Context for the following reasons:

I presented the state management options and we went with React Context using the approach that I’m going to outline below:

So if you’re already bored with my intro — the tldr; version can be found here. The code below is built on top of Create React App for brevity & simplicity.

For the purposes of this overview, we’re going to put all the code relating to our global state in a single file calledGlobalStateProvider.tsx. You probably could abstract the code into multiple files / folders that better fit your structure.

When creating a typed global state with React Context the first step is creating the interface to that state. Inside our GlobalStateProvider.tsx:

Next, we need to create the context provider:

This creates a default context object with a property of state that takes the shape of a Partial of our GlobalStateInterface that we declared above. It also takes a property of setState which is an object (a.k.a. a function) that is of type Dispatch<SetStateAction<Partial<GlobalStateInterface>>>. MAN THAT WAS A MOUTHFUL!

The observant of you may have noticed that these types mirror those of the useState() hook array. Well noticed! This is because at the core of our simple-global-state™️ we are using useState(). You’ll see below.

Next we need to set up the context Provider. This is the component that we use as a parent component around any child components that need to access the global state.

In this snippet we are effectively creating a wrapper that can be used at the top level of our app. It makes anything we put into the value prop object available to any children. You can see here we are passing the state value and setState function from React’s useState hook

Of particular note in this snippet is that we are providing the value parameter a default value of an empty object that takes the shape of our GlobalStateInterface. This allows us to pass a default initial state at the top level where we consume our GlobalStateProvider. This is very useful for using the wrapper when testing!

Ok so we’ve done all the boilerplate-y setup, we’re now onto consuming & setting our state 🎉.

In this snippet we are creating a hook that unpacks the GlobalStateContext using React’s useContext hook and returns an object containing our state and the setState function (as we did above)

Ok so you’ve made it this far…congrats! Here’s what your GlobalStateProvider.tsx file should look like:

Now, in your application you need to wrap all the pages / components that need to use your state, I did this in the App.tsx beneath the React Router <Switch> component:

And now in your child page page you set your state (see line 10 & 13)

And to read your state:

And that’s it! Probably my favourite thing about this method of state management is:

If you check out the repo, you can also see the BONUS CONTENT of a <Debug> component that writes your state object into a viewable format. It’s something of a poor man’s redux-dev tools 🙈.

Image for post
Image for post
That’s All Folks! © Warner Bros., via Wikimedia Commons

I hope you enjoyed this article, and hopefully it helped you in creating simple-global-state™️️.

What I haven’t covered in this article is the performance issues of using this method for large scale global state. It’s quite well known that using React Context for high frequency updates & in multiple components can cause issues, primarily because anything that consumes the React Context state gets re-rendered. This state management is meant for small typed JSON objects to be passed between multiple pages in low-frequency updates, (ideal for a form).

If you have any questions or comments feel free to leave a comment below, or ping me on Twitter @JamieADHaywood.

Entrepreneur & Lead Developer @ Captur.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store