Easy typed state in React with hooks and Typescript
This post expects you to have had minor exposure to Typescript. If this isn’t the case I would recommend that you firstly read an introductory post, such as this one. Not required, but certainly will help.
React hooks are officially a thing. Woot! I previously wrote about Easy Peasy, a global state library for React that leverages hooks and focuses on having a super simple but powerful API.
This post is going to take things to a whole new level by showing you how you can compliment this API with the power and safety offered by Typescript.
I am going to take us step by step through building a naive todos application. Whilst this is completely unimaginative, I hope its boring familiarity will allow readers to more easily focus on the typing and APIs.
The code for this post exists in this GitHub repository.
Right, let’s get to it.
Set up
To get going as fast as possible we’ll leverage Create React App, along with its Typescript support.
Firstly, bootstrap an application, specifying that it support Typescript.
npx create-react-app type-safe-state-ftw --typescript
Ok, change directory into your newly created application.
cd type-safe-state-ftw
Then install Easy Peasy.
npm install easy-peasy
This single dependency includes everything you to support robust global state within your application. Some of its features includes:
- Derived state
- Remote data fetching/persisting
- Auto memoization for performance
- Simple mutation based API that converts to immutable updates
- React Hooks to use with our components
- Full Redux interoperability (including Redux Dev Tools)
And it only carries a very respectable 9kb gzip cost — all dependencies included.
Defining our Model
We will start by creating types to represent the model for our store.
Fairly self explanatory. We will have some todos along with a notification msg.
Create our Store
Now we can use the StoreModel
whilst creating our store.
By providing our StoreModel
as a type parameter on createStore
we get two things: type checking that ensures our implementation matches the StoreModel
type, and auto completion whilst we do so.
The returned store now has all the type information baked in, so any interaction with it will have Typescript helping us.
FYI —
createStore
returns a Redux Store, with all the Store APIs being available against it.
Grabbing state directly off the Store
Sweet, let’s try grab some state directly of the store.
This operation is fully typed.
Zing.
Defining actions to mutate state
Actions are responsible for updating the state on our model. Let’s expand our model types to indicate where we intend the actions to exist.
The Action
type is a generic type, where its first type argument is the model that it is operating against, and the second type argument is the type of the payload it will receive.
It’s also completely fine for an Action
to contain no payload.
Implementing actions to update state
After updating our StoreModel
Typescript will be shouting at us that our store implementation does not meet its expectations.
Let’s implement the missing actions against our store.
You can see the actions receive the state they relate to as well as any payload that was provided to them. We use the actions to update our state — under the hood these mutations are converted into an immutable update against our store.
FYI you can also return “new immutable state” within your actions if you prefer. I prefer the “mutable” form as it’s less error prone and more concise, but this is a matter of personal preference.
We will get type checking and autocomplete whilst implementing the actions.
Firing actions directly via the store
Similar to accessing state directly against the store, we can also fire our actions via the store. They are bound to the store’s dispatch
at the same path as they were defined within our model.
And yep, typed again.
Expose the store to your application
Before we can use our store within our React components we need to wrap our application with the StoreProvider
, providing the store
instance.
Almost there.
Getting our hooks ready
Easy Peasy ships with hooks to interact with the store within our React components. However, it will be great if we can ensure that the hooks contain all the type information about our store so that we don’t lose any of the type safety and autocomplete goodness we have gotten used to.
We can create pre-baked typed versions of our hooks via the createTypedHooks
helper.
Now whenever we need to use a hook from our hooks.ts
file they will be able to guard and guide us via the type information we provided to them.
Hooking it all up
The moment we have been leading up to. Here’s how we use our state and actions within our components.
This Todos
component pulls out the todo items from the store, and subsequently renders them in a list. It allows us to add a new todo item. You will note that we not keeping our form state within our store. I highly recommend that transient state, such as form data, be created and used within the scope of your components themselves. In our example above we use the standard useState
hook that React provides to help us with this.
This implementation is very naive, and could definitely do with some more work, but I’ve intentional kept it simple to make it quicker to read and understand what is going on.
As we consuming our typed hooks within our component we get all the type benefits.
This can be hugely helpful when refactoring your store as you will immediately get error messages in any component that is consuming refactored state — allowing you to quickly cycle through the instances and fix them.
What about side effects?
We can encapsulate operations such as network requests within “thunks”.
Let’s adjust the requirements for our store. We now need to ensure that any new todo items are sent to the backend via network request before adding it to the store.
We can use theThunk
to describe an action that will allow us to encapsulate this behaviour.
We have done two things here. Firstly, we added the save
thunk action, represented by the Thunk
type. We then renamed the existing add
action to saved
. The thought process being is that we will first persist our new todo via the save
thunk action, and subsequently add it to the store via the saved
action. It will become clearer later on why we have a pair of actions for this.
Let’s update our store implementation accordingly.
As you can see the “thunk” action doesn’t receive the model state. Instead it receives the actions of the model it relates to. Therefore if we wish to update state we need to dispatch other “standard” actions to do so. This model is very much the same as found in popular libraries such as redux-thunk
.
Within our save
thunk action we make a call to our todoService
, then we dispatch the saved
action to ensure the todo is added to our store.
We can now update our component, ensuring that it calls the newly created save
thunk action instead.
What else?
There are many other features — which I will avoid going through right now out of fear of creating an overly long post. Some of the additional elements you can declare on your model include:
- Derived state, where they are automatically optimised/memoized
- Listeners so that one part of your model can respond to actions being fired from another branch of your model.
I encourage you to read the Easy Peasy GitHub page for more information about the library itself, view a more extensive example of the Typescript integration, and get detailed information in regards to the APIs.
In case you missed it, the code for this post lives in this GitHub repository.
Thanks for your time
I hope you enjoyed the read. Any and all feedback is greatly welcomed.
Feel free to log any issues via the GitHub repo.
✌️