Today we will dive a bit deeper into the way I work with redux and why I think that, despite recent developments of react hooks, redux still has its place in our infrastructure.
I’ve got multiple requests for more information about my redux setup after writing my last article so I decided to make a small deep dive into the topic this week and I will probably follow up with another one in the next article about routing with redux and typescript, should I get a similar response.
As always, you can find the GitHub Link to the finished code at the end of the article.
SERIES: React Bootstrapping
1) Quick Start with Typescript, Linting, Redux & Router
2) => you are here <=
What is redux?
Redux is a predictable state container that uses the concept of actions and reducers to change data in a single source of truth, the redux store.
Wow… This sure sounds important but what does it actually mean? What is the state of a website or, for that matter, a store?
The redux store is nothing more than an object containing our websites data and UI state, ideally in a normalised form. If, for example, a user navigates to the article listing page of our website and filters the listed articles by a topic, our state contains the following:
- Navigation state: the current page/URL
- Data state: the list of articles (response from an API call)
- UI state: the current filter mask
Actions are messages of intent. An action does not change any data in our stored state. An action is nothing more than a message containing an identifier and a payload to be handled by a reducer.
- If the user navigates to a page, we dispatch a navigation action and the navigation updates
- If the page wants to load data from the website, it dispatches an API action
- If the user selects a filter in the UI, an action is dispatched to update the filter for the currently displayed data
Reducers handle all incoming actions and decide on applicable state changes. If a dispatched action has a matching reducer, the reducer will check the action’s identifier (type), accept the data (payload) and make changes by creating a new state. “New” is important here, in redux we do not mutate the state. Every action creates a new state.
Redux also has an optional third layer called middleware. The middleware is sitting between actions and reducers, reads every action passing through and handles it based on the content.
This (optional) part is the most interesting in my opinion. I usually have zero logic inside my actions and as little as possible in my reducers. Actions are nothing but messages with an intent and reducers only handle the state change in a predictable way. They should be as clean as possible, returning the same output every time they receive the same input.
Another thing to keep in mind when you decide on whether or not to use middleware is that redux is synchronous. If you want asynchronous interactions (like API requests), the middleware is the right place to implement this. A few common examples of middleware are the following:
Filter middleware looks at incoming actions and makes decisions based on its own logic. You might, for example, want to throttle user actions that result in an API request or UI change. In that case, you can do so here, only letting an action through every n seconds.
Async API requests
API requests are another use case. Let’s say the user loads a list of articles. The article action signals its intent to load data from an API as well as callbacks to execute on the success and failure of the request. The middleware will let the action pass through so that the message is following the correct flow and then dispatches a neutral API action with the payload of the original request.
The API action does not need to know about the source of the request (our articles page) and only cares for the requested data and URL. This way you only need to write and test the API logic once and it is fully reusable. Once the API request is resolved, the response is passed on to the success action (store articles) or error action (handle a failed request).
This might sound verbose and like a lot of actions are dispatched for a simple request of data from an API but it allows us to look at the state of our app and the flow of messages and see exactly what happened.
[ARTICLES] Request article List
[API] Request data
[API] Request success
[ARTICLES] Store articles
If you take it one step further, you might want to update your UI based on the loading/pending request. In that case, you would set up the articles middleware to trigger the API request and update the UI accordingly.
The middleware would then “split” or dispatch multiple separate actions and the action/message flow could then look like this.
[ARTICLES] Request article List
[UI] Set page loading
[API] Request data
[API] Request success
[ARTICLES] Store articles
[UI] Set page idle
The official redux guidelines recommend a different pattern where you write one action and multiple reducers handle it accordingly but I recommend not to do so.
Don’t get me wrong. I, too, prefer to write less code and chose to work with redux toolkit, exactly for this reason, but dispatching more actions and handling them separately, turning your application into a message-based system, has its benefits regarding scalability and readability, two qualities that can make a big difference in the future if your project.
If you follow the path described above, the separation of your project’s different concerns is much clearer and is following well-established design patterns developed and described many years ago by people (links at the end of the article) with loads of hands-on project experience.
We have outlined where we want to go with our project so let’s see what we need to get our article page working with redux.
I’ve prepared a new project using create-react-app and a typescript template. I’ve also added some linting and an article component to showcase our store.
Right now, we are displaying a list of articles that is hardcoded. We want to move this to a mock-api server, which I’ve prepared, including the mocked server response for our articles.
You can check out the prepared project including the mock api server here to get started.
Additionally, we will work with a pattern called redux ducks. A duck is a single file containing all the actions, middleware and reducers needed to define and handle one slice of our stored data.
You can read more about the Redux ducks proposal to get a better idea of what our file/directory structure is based on.
In addition to starting the web project using
npm start, we also need to start the mock server, which is a separate and simple express server. To do so, simply run
npm run mock-server in a separate terminal tab. You could also chain both in a combined script in the package.json but I prefer them to run in separate tabs so that their logging is clean and separated in case of issues.
We will start by defining our imports and types. The Article type can be copied from our articles.tsx while the rest is new.
For our actions, we need to be able to
- request articles
- store articles
- set the status of the UI
- handle a request error
In our middleware, we will match all actions that match our requestArticleData action, to dispatch the UI update and send off the API requests via API actions. We tell the API the type and target of our request and what to do with the resulting success or error. This is the splitter pattern, we talked about earlier.
We also match for cancelArticleRequest actions because we want to both log the error (for now temporary to console) and update the UI to cancel the “pending” state.
Our last bit here is the default export for our articleReducer. We only need to handle actions that either store the article data or simply update the UI state.
Our API code is not trivial and I’d advise you to simply copy it for now if you feel like you still need a better understanding of how redux with typescript works but I’ll try to outline the basics.
First of all, we need to define our API endpoint (API_HOST). Our example assumes that there is only one and it’s currently set to our mock API server.
Then we need to define all different types of requests (“GET”, “POST”, “PUT”, “DELETE”) and how an API payload is structured including onSuccess and onError actions.
Our actions are relatively simple, now that we have defined all the typings above. We have our apiRequest as well as the apiSuccess and apiError actions.
The final part here is our middleware because the API does not have a state in our store. Our middleware is responsible for resolving the request via fetch and handling the success and error cases with the respective actions, dispatched with the original callbacks from our article action.
We now need to register our reducers with the rootReducer and add a rootMiddleware to register our new apiMiddleware and articlesMiddleware.
To add our middleware to the store, we can append it to the already existing default middleware that redux toolkit brings to the table. We also need to make an exception to our serializableCheck middleware (from redux toolkit) because our onSuccess and onError actions are not serializable.
We already have our redux provider component as a wrapper around our app (part of the prepared setup I made) but right now, our Articles.tsx component does not know how to access the article state.
In the past, it was common to use the connect() function from react-redux to allow components to access the state but with the advent of react hooks, this changed. We already have a typed useReduxDispatch and useReduxSelector hook (also part of the prepared setup) and could use them directly in our Articles component but I personally prefer to keep them separate in a .hooks.ts file for each component.
We will create a new articles.hooks.ts file next to our articles.tsx component file and add our redux interaction there to keep our Articles component as clean as possible.
With this in place, we can clean up our Articles.tsx and remove everything by replacing all the state logic with our new hook.
With all said and done, we’ve successfully hooked up our website with a mock API using a clean and scalable message pattern in redux, allowing for a readable and easy to understand message flow.
There are two minor changes I did not show in the article above. For one I had to adjust my linting to allow for imports using ‘./articles.hooks’ as the linter thought .hooks was the file ending… we can’t have that.
The other thing I did change was adding a key attribute to my list of articles because react always needs a key attribute when rendering lists of elements.
Links and Recommendations
Nir Kaufmann has a great presentation on Advanced Redux Patterns on YouTube, and while he does some things differently, you will be able to see the same patterns emerge.
Additionally, I can really recommend this old book because many of the ideas and concepts are still relevant today.
Next time we will look at routing and page navigation in detail before we move on to applying layout and styling our app with styled-components and themes.
Some words about me:
If you want to see more of my work and progress, feel free to follow me and check out my other articles. If you clap feverishly for the articles you like most, it will be easier for me to decide which directions to pursue in following articles so use your ability to cast a vote for future content.
I’m also currently working on other series covering complex React Native Setups using Typescript and scalable apps with Redux, where I’ll go into details about how and why I do stuff the way I do as well as some articles on my experience in building games for web and mobile with React.
Here are some of my recent topics:
- React Quick Start with Typescript, Redux and Router
- Linting/Prettier with Typescript
- Redux + Toolkit with Typescript
- clean and simple Redux, explained
- Game Theory behind Incremental Games
- Custom and flexible UI Frames in React Native
And if you feel really supportive right now, you can always support me on patreon, thus allowing me to continue to write tutorials and offer support in the comments section.