In this article I’ll explain how Redux Toolkit simplifies building Redux based apps and combines beautifully with React and TypeScript for an all-round great stack.
Why am I talking About This?
I also know that in terms of languages, TypeScript is very popular (although not necessarily with React),.
Looking at managing data, Redux is also popular but an interesting figure below is the 19.8% who’ve used Redux before, but would not use it again.
Also, looking at Google Trends, the number of searches for Redux dwarf those of Redux Toolkit.
So, in this article I’m focusing on,
The benefits of decoupling state from view generally
A quick review of how Redux does this
Using Redux Toolkit to simplify Redux
All presented in the context of React and TypeScript
My hope is that by the end of this article you’ll find Redux less intimidating and you’ll be encouraged to try it along with TypeScript for a React project. The sample app I’ve created for the article is a basic web-based calculator,
I’m assuming you have basic working knowledge of React and TypeScript (although perhaps not together or not in anger). If you’d like deeper dives into anything I cover, please Like and leave a comment below.
Decoupling State and UI
React is a framework entirely focused on building views, and it does that very well. The succinct syntax and interleaving of JSX view descriptors with standard programming elements facilitates easy construction of a component-based UI.
What it doesn’t do so well is handle state. Coupling state to our views and passing state through large trees of components is fragile and clumsy. Whenever state is owned by components, more business logic will reside in them which compounds the issue.
A much more elegant solution to separate our state and domain logic from our views. Then we can arbitrarily choose which components to bind to — we only bind what is needed, where it is needed.
Decoupled, the state and domain logic are easier to develop. Removing view concerns makes debugging and testing much easier and allows us to focus on simple data transformations. We can model our system as data and descriptions of how our data changes based on events entering the system. This allows us to greatly reduce the cognitive load as we build more complex applications.
The view is also easier to work with as components are smaller and focused on simply rendering a template based on some state. It becomes easy to restructure the UI, splitting or coalescing components. We can move and share where state is utilized within the tree easily as we can arbitrarily bind any state to any part of the view. For example, duplicate a basket total from a component at the bottom of the screen to a toolbar at the top or utilize a busy status across several components.
Redux is a common state management framework and is often paired with React (which also promotes the functional and immutable thinking that Redux favors).
In the Redux model we have,
Store – this contains and manages our state
Actions – describes a state change (you may think of this as an event)
Reducer – a pure function that takes the current state, an action and will compute the next state
So, the general flow is that we start with some initial state and then we can dispatch actions to the store which will use the reducer to compute the new state. The view listens to the store and is notified when something changes.
Without going into too much detail, the system relies on the immutability of the state data structure which is why the reducer is a pure function (no side effects). When we have immutable data structures, checking if something has changed (and therefore knowing if the UI must rerender) is trivial as it is simply a reference comparison – to be different, it must be a different object.
Considering our Calculator app, how would we present this in Redux?
State – What will the system need to represent?
Actions – What events can happen to our application?
Reducer – Process action + state to produce the next state
Implementing the actions, reducer, and state in normal Redux requires writing lots of boilerplate code. The pure function and immutability constraint on the reducer also further complicates things.
In the past, I’ve written my own helpers to alleviate some of this, but the Redux Toolkit is now here to remove the need of doing this yourself. It is easily added to a project,
npm install –save @reduxjs/toolkit
What’s great is it comes with TypeScript bindings included.
Actions, Reducers, and Slices
Toolkit provides helpers for creating standard elements – store, reducer, action, async actions, etc. Even more useful though is the concept of a Slice, which allows us to set up our state, reducer and actions in one container.
The ‘reducers’ object defines functions that both define an action and the reduction logic for that particular action. What’s more, the reducer function can be written in the immutable style, returning a new state, or can be written in a mutable style.
The toolkit uses another library, Immer, so that the function can be written using simple mutation. Behind the scenes, Immer, will pass a proxy state object, track changes, and then perform the immutable transformations required. This can greatly simplify some operations like making changes in deeply nested object structures or arrays and other data structures.
The slice generates the pure reducer functions and a collection of action creator functions that can be exported. For example, with our calculator we end up with,
The great thing about all of this is it is type safe (and not with very much extra annotations). The action creators generated, such as keyPressed, will take the correct parameter types based on the function definition within the reducers section.
We can also include actions created externally (either in another slice or using the create action helpers) within an ‘extraReducers’ section of our ‘createSlice’ params..
Creating a store is also easier with the toolkit. The ‘configureStore’ function creates a store using a reducer just like the old ‘ createStore’ function but it wires in useful middleware by default. Middleware is the way Redux stores can be extended to process actions in a centralized manner. By default you have none but with the toolkit it wires in:
Redux Thunk – allows us to submit functions as actions for asynchronous actions
Redux Dev Tools – enables the Redux Dev Tools browser extension
Immutability Invariant – ensures that state transitions are always immutable
Serializability Invariant – ensures action and state are always serializable.
The bottom 3 are automatically only enabled in debug mode.
So far, I’ve been able to develop the core logic of the app without focusing very much on the view. This separation of concerns is very powerful. It also facilitates easy testing as the reducer function is pure and we simply need to specify inputs and outputs.
Leveraging the fact that we define tests by calling the ‘it’ (or ‘test’) function we can write parameterized tests easily by using a forEach call,
Again, all of this is nicer in TypeScript as the test data object can be any shape but the compiler still knows the type and will give us error-checking, auto-complete, type checking, rename refactoring etc.
Note, within Jest, it and test have parameterized forms built in, but they are not a nice for complex test data as using the forEach.
Connect to React
Now that we’ve covered building out our state and domain logic, let’s dig into the view. The toolkit doesn’t help with connecting the state to the react views, but the core react-redux library has itself evolved to support Redux Hooks. This makes it easy to connect any part of our state to components using the useSelector hook function.
useSelector hook, not only are we using this state when we render, we’re setting this component up so it will automatically re-render if this state changes. Note, it is not if any state changes, just the extracted state.
Likewise, it is easy to dispatch any action using the useDispatch hook
Notice as well that this component is strongly typed. React has great TypeScript support too and we can type our functional components with ‘FC’ and add an optional props type, in this case ButtonProps,
TypeScript will ensure the props are correctly typed, easily destructured and that our components are used correctly within JSX.
I hope that this article gives you some insights into using React and Redux together in a modern setting, utilizing TypeScript and the Redux Toolkit to increase safety and reduce boilerplate. There’s lots more detail that I could go into so please Like the article and leave a comment below for other topics for future articles.