Typesafe Global State with TypeScript, React & React Context

Jamie Haywood
5 min readNov 23, 2020

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.

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:

  • It comes baked into React itself — no extra yarn install, no extra bytes in the final compiled version being served to users.
  • It’s well documented by React — most of my personal questions
  • Easy to create a simple hook abstraction for inexperienced React developers — this was probably the most important reason in my mind. We could create a pattern for getting and setting state that was not complicated, and required little to no learning curve to use.

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:

  • It’s simplicity — it’s almost exactly like using the useState hook, and therefore makes it very accessible to developers not yet familiar with global state management tools, but familiar with setting local state.
  • It’s type-safety — the compiler will throw an error if you try and put any content in your object that you didn’t define in your interface 😄🚀

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 🙈.

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.

--

--