Typesafe Global State with TypeScript, React & React Context
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.
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 🙈.
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.