End-to-End Redux in Plain English
Context
If you expect your application to go big, with a few complex features, and many interaction across different components, perhaps you should use Redux or a similar state management tool from day 1, as it enables you to manage the complexity of data exchange between components robustly.
On the other hand, using Redux for simple applications won’t be necessary at all. If your application is going to remain small and simple, using Redux will be counter-productive, as Redux by itself, adds unnecessary complexity to the app.
However if you decided to use it, it is a powerful tool, and it helps you with the state management across your app pretty effectively.
By the way, this topic belongs to the series to set up a Single Page Application using React, Redux and Asp.NET 5.0.
- Styling and Theme Management
- Global Navigation
- Responsive Layout
- Forms and Input Validation
- Routing
- Configuration Management
- API Call and Activity Indicator (Spinner)
- Monitoring, Logging and Instrumentation
- Toast Notification
- Redux and Store Setup
- Google Maps with Polygons
- SEO — Search Engine Optimisation
- Running React App on .NET Stack
- Deploy React App to Azure App Service
What is State Management anyway?
State management is all about sharing data between different components in your app. Let’s cover it with a real example; consider we have the below app components:
When the user logs in, the profile information gets filled consisting of Contact and Payment details. Payment Details will be used in other parts of the app as well, such as when placing new orders. The question is assuming you already fetched user’s payment information as part of their login, how would you feed that to another component such as New Order?
Options
Here are the options I might have:
- Fetch Again: Make a new query to the backend and fetch it again.
- Pass it On: Pass the PaymentInfo all the way up to the App Root from PaymentDetails component (using callback functions), and then pass it down again to the NewOrder component (using props).
- Central Storage: Have a central storage that can connect to all the components, and once information is available in that store, it is available to the rest of the components too.
Option 1 — Fetch Again — comes with this inefficiency that we re-fetch the information I already have. Option 2 — Pass it On — means this complex flow of information within my app, that won’t be maintainable when my application grows in size. However Option 3 — Central Storage — sounds pretty efficient as every component can quickly plug into that storage and fetch data cheaply.
Tools
As it is evident, Central Storage solution to share information across React components sound quite scalable, even for complex products with many components. There are various options out there to implement the central storage solution. Two of them are:
- React Context: is designed to share data between many components at various levels of component tree.
- Redux: is a third part library to achieve the same purpose, and that is the focus of this post.
How does Redux work?
Redux can have a steep learning curve, and in my view some of it is due to how they have named its components. Here I am going to take a unorthodox approach: I will explain everything step by step by following the flow of data instead of explaining all the theory upfront, and then implementing it. I will also explain the theory behind the namings as I go, and I hope that’d make it easier to grasp Redux.
To make it real, let’s do that by using a real feature, such as a Login form, however to keep it brief, I won’t cover the UI elements. If you’d like to have a look at how I manage forms, refer to the Guide to Forms and Input Validation in React SPA.
In a nutshell here are the steps at a high level when building a login form with Redux, after you laid out the UI elements on the page:
- Connect the login component to Redux store
- Send a query (request) to the store to log the user in
- Perform a bunch of operations on the query such as sending an API request to the back end
- Receive the outcome of the query from backend API
- Pass it on to Redux and Update the store with the outcome
- Act according to the store’s new data (i.e. go to user profile, or show a message)
This is the flow, and I will explain them one by one in theory and code.
1. Connect Login Component to Redux Store
If my React component wants to read data from the central store, and execute queries against the store, it needs to be connected to the store. Think of it as connection your API has to the database, it’s not exactly that, but you get the point.
The way we do it, is through a Higher Order Component (HOC) called connect. Connect gives me access to the store, and enables me to run queries against the store. Let’s look at the code:
Execute Commands: Starting from the login form, when the submit happens, we call the logMeIn function. Connect has introduced a new function (logMeIn) into my props, which maps it to the loginCommand in accountServices. This has been done using mapDispatchToProps construct. You can think of loginCommand as the command I want to run against the store to login the current user with the entered username and password.
Read Data: On the other side, connect enables me to read the available data from the store. In this case as an example, if the login operation failed, I’d need to show the right message returned from the server on the form. Again, connect has mapped my store data (state.accounts) to current component’s properties using mapStateToProps construct.
2. Send a Query (command) to the Store
In the context of our Login Form, according to the above snippet, calling this.props.logMeIn means I actually dispatch whatever I return from loginCommand in accountService.
Dispatch simply means I send a command with a specific payload to the store, so nothing complicated.
Before dealing with the dispatch, let’s see what is being returned from the loginCommand.
All it returns is a json object, that contains a command type, and a payload. In this case, the command is telling the store, that the type of this command is an API call to the backend, and to make that API call, some of the attached information (baseURL, URL, data, and method) are needed. It also tells the store what to do, after the response was arrived from the backend (using onStart, onSuccess, onError), which I will explain shortly.
Summary So Far: I have established how to connect your component to the store, and talked about how to dispatch a command to be run against the store, and what that command needs to look like.
Note
Configuration, plumbing and troubleshooting your software foundation take a considerable amount of time in your product development. Consider using Pellerex which is a complete foundation for your enterprise software products, providing source-included Identity and Payment functions across UI (React), API (.NET), Pipeline (Azure DevOps) and Infrastructure (Kubernetes).
3. Perform a bunch of operations on the query
Think of the middleware as a series of operations that would run on each query. Once every operation finishes, you pass the query to the next operation in the pipeline, and hence the order matters.
I am still not going to talk about how to configure your store, however below image shows how it is structured:
Figure 3 shows you how the middleware operations are grouped together, and as you can see I have the below operations to run on each store query:
- API: if the query involves making an API call, this middleware is responsible for it, and based on the payload information in the query, executes the API call against the backend system.
- Logger: If the query needs to also be actioned by the logger, this middleware will take care of its details. For more information on logging, refer to Monitoring and Logging for SPAs.
- Toaster: Any necessary notification will be dealt with using Toaster middleware. For more information on Toast Notifications, refer to Toast Notifications for React SPAs.
- routeManager: Any route changes which needs to be made as as result of this operation, routeManager will take care of that.
As I mentioned, the order we set for our middleware pipeline operations matters, and hence I will probably put API Call first, followed by Logger, Toaster, and Route Manager.
Below I am including the code for each middleware, and I will explain in detail what it does.
API Call Explained: As I have covered before, API call takes the command and if it involved an API call, it makes one. Going back to our loginCommand from accountService file, we had the below action dispatched against our store:
I have set the action type for the loginCommand as “api/callBegan”, which means this command involves an API call. Now if you look at the below API Call middleware, at the beginning I am checking for this, to ensure I only execute this middleware for such actions.
If that condition is not met, this middleware will be skipped, and the query is passed to the next middleware (in our case Logger).
For more information on the implementation of API Call, refer to Making API Calls for SPAs.
You might notice there are three types of dispatch operations within this middleware:
- dispatch(OnStart): Executes a certain operation which needs to be run before any action is done by the API Middleware.
- dispatch(OnSuccess): Gets dispatched when the API call returns a success response.
- dispatch(OnError): Happens when the API Call fails
But where do these came from, and where do they point to?
The answer is simply within the loginCommand payload, they come from there, and you are directing the API Middleware to call methods associated with those pointers. So I answered where they come from, the next question is where those methods are?
The answer is they are also located in the accountService, with the below form, called Reducers (be patient, I will explain this confusing topic deeper shortly):
Reducer
It is a function that changes the state of your application in a consistent way. It means no matter how many times you run this function at a particular application state, your application state after that will be the same. They are basically pure functions.
Looking at the above snippet, I have three reducers for accounts. One is run when the API middleware dispatched the OnStart (“account/dataOperationRequested”), one is run when the API middleware dispatched OnSuccess(“account/loginSucceeded”), and one is run when the same middleware dispatched onError(“account/dataOperationFailed”).
Summary So Far — The Flow
My React component sent a command, loginCommand, to the store using connect method. The command was dispatched with an action that had a type “api/callBegan”, and a payload to call the API, and also to tell Redux what to do in case of Start/Success/Failure. Once each scenario gets executed, a pure function (or a reducer) associated with that scenario gets run to update the state.
One other use case for these three phases of start, success, and fail is how you want to enable and disable an Activity Indicator or Spinner. Basically when you start an API call to backend, you want to the spinner to appear and if it failed/succeeded, you want it to stop, showing the end of the operation.
Looking at accountReducer again, you can see we have an initial state for accounts, which we can provide it right here, but the point I am trying to make is that we now have a space in the application storage called “accounts”, which we initialise it with the below json value.
Store Structure
I talked about the “accounts” space in my application storage, and I’d like to point out that we can have as many spaces (slices) as I’d like in my application store. Let’s look at the below image, which is the structure of my app store:
Also let’s look at the file structure of my Redux setup again:
As you see, I have divided my app store into 4 slices (accounts, contactDetails, comms, locations), and for each I have a dedicated set of reducers like what I went through for my “accounts”.
This architecture enables me to divide the services behind my app, in addition to my store, into logical slices, which in turn makes my application very scalable and capable when it comes down to dealing with complexity.
Note
Configuration, plumbing and troubleshooting your software foundation take a considerable amount of time in your product development. Consider using Pellerex which is a complete foundation for your enterprise software products, providing source-included Identity and Payment functions across UI (React), API (.NET), Pipeline (Azure DevOps) and Infrastructure (Kubernetes).
Store Setup
Now that we know the flow, the logic behind the flow, and the terminology let’s set up our store.
I talked about dividing your app store into logical slices, and for each slice to have a set of reducers grouped under that name (“accounts” and accountService). When we want to construct our store to contain all those slice, we should combine the reducers and return them as one, to represent the entire store, which I have put in entities.js file.
And one important piece of puzzle, which many people cover first, the store configuration and set up.
I will explain this bit by bit.
- createStore: is the function that creates the store for you. Now, it is easy for you to guess what goes into it; all the reducers (services, such as accountService), an initial state for your store, and the middleware operations.
- PersistStore: One behaviour you might notice when you first set up Redux, is the fact that you will lose your app store content if you refresh the page or close it. Redux Persist helps you preserve your store content across page refresh or tab close. In this case I have used session storage (as you see in persistConfig) so if you refresh the page, you keep your store content.
- devTools: DevTools helps you see the content of your store visually through a browser extension called Redux DevTools as it changes from one command to the other. You can see in the below image that it shows you the content, and also all the actions executed against your store (like history). It is a Must Have tool for Redux development. As you can see in the code I have disabled it in production, and it is only applied to test environments.
Inject the Store
Once the store is setup, the last step is to inject your store into your component lifecycle. Below code simply just do that:
Pellerex Foundation: For Your Next Enterprise Software
How are you building your current software today? Build everything from scratch or use a foundation to save on development time, budget and resources? For an enterprise software MVP, which might take 8–12 months with a small team, you might indeed spend 6 months on your foundation. Things like Identity, Payment, Infrastructure, DevOps, etc. they all take time, while contributing not much to your actual product. These features are needed, but they are not your differentiators.
Pellerex does just that. It provides a foundation that save you a lot development time and effort at a fraction of the cost. It gives you source-included Identity, Payment, Infrastructure, and DevOps to build Web, Api and Mobile apps all-integrated and ready-to-go on day 1.
Check out Pellerex and talk to our team today to start building your next enterprise software fast.