by Abhinav Jain
React + Redux Architecture: Separation of Concerns
Is having a basic understanding of React and Redux good enough to build an application? No, it’s definitely not. This is not how we do it!
One of the most important aspects required in the architecture of a React-Redux app is SEPARATION OF CONCERNS.
Why do you need this?
When I started working on a React Redux application, I looked for blogs or articles on this topic. I wanted to find the right approach towards the architecture of an application.
But I couldn’t find any articles which could connect everything. So I have come up with everything based on my experience.
Do not start your app by designing the component and the reducer structure. You need to take a lot of things into account before doing any of that. There is one thing which everyone must follow:
Never compromise on quality.
Let’s start with the basics.
But why Redux?
The router is very significant, because it’s an entry point of your application. There can be several routes in your application. You would need functionalities like validation, authentication, redirection, etc. depending upon the requirement. Handling them will come in the Redux part, which is covered later in this blog.
React Router Redux is the most popular library for routing.
Now before we start discussing architecture, it’s very important to separate concerns for both React and Redux. If we don’t do this, we might make small mistakes which could lead to big flaws in the flow of our application.
SEPARATION OF CONCERNS
Here, the two sections are React and Redux.
We want to make sure that our app is maintainable, extensible, flexible and reusable.
These are key points which will help you decide what part of the app should go to the data part and which part to the representation part.
What it should do
- Render HTML content with the data provided.
- Have multiple UI states depending on the data, so that you understand why it is shown.
- Dispatch actions on user interaction or life cycle events (conditional)
- Animation using ReactCSSTransitionGroup or any other libraries.
What it shouldn’t do
React is a representation library. It should give a view of the data provided to it and nothing more than that.
These points will help you understand that:
- It shouldn’t request data, if it’s not present.
It’s not a part of the representation. The Component should just display the UI, for the data that is available. If the data is not present, show a default state or an error state depending upon the scenario.
For example: Let’s take a TODO app. We have a React component which should show details of a TODO with ‘id’ 6. The component shouldn’t have a check if the data is not available. It should dispatch an action to fetch the data.
The best solution will be to fetch data from actions. The component will take care of rendering HTML content based on data provided.
But the question here is: who will dispatch the actions if components don’t?
We know that the entry point to your application is routes. When a route is matched, a component corresponding to that route is loaded. The Route handler for that route is called. So, the route handler will take care of dispatching that action.
- Storing data in its local state (can be used if it’s an animation or a user input)
You may store user inputs like text and number. It’s still best to keep it in redux store. You are never sure when the data of the component will be used by some other component. And then you’ll have to keep pushing the data to the top container components so that it can accessed by lower ones. To avoid all this, store everything in the redux store.
Let's take an example of a timer. We have to display time taken for a quiz. We have a local state which is set to 0. On componentDidMount, we have a timer running which sets the local timer value each second.
This will work fine if you don’t have any other requirements. But this will fail in these 2 cases:
- If we have to show an alert when the timer has crossed 120 seconds. We don’t have the timer information outside the component. In this case, it will fail.
- If someone switches to another screen where the Timer component will be unmounted, the data will be lost. So, when they come back to the screen, a component will be mounted again and the timer will start from zero, which is not what we want.
The best solution will be to have the timer running in actions and let it update the store each second. Then, every component will have the access to the time value and doesn’t matter if the component is mounted or not.
- Shouldn’t dispatch actions in a component’s lifecycle events if the action is not directly related to the lifecycle event
Life cycle events, if not used properly, can lead to so many issues which might not be easy to debug. Even if you are able to find the issue, you might have to make many changes. This might make your component even more complex. You might end up completely changing the structure. So, be very careful while using life cycle events.
Let’s take a TODO app to get a better understanding. We have a component which shows a list of todos which dispatches an action from a life cycle event.
Have a look at the 4 diagrams below.
Here, in the example:
- Fetch list details if not available: This is not correct because TODO LIST will not be a reusable component anymore. It will always be dependent on one type of data which it is trying to fetch. Also, we will have to add a condition to check if the data is available or not. It’s doing more than just a representation.
- Start animation of header component: This is wrong because the event is related to a header. If the action has to be dispatched, it should be dispatched from the header component’s life cycle.
However, the best solution would be to dispatch the animation action from the entry point. It could be when the route is hit or any action is dispatched to show this UI which contains the header component.
- Todo List viewed by user: This is the correct example. When the todo list is mounted, a user gets to see the list, and that is what’s being updated through the action.
- Reset UI state on component unmount: This is a very good use of a component’s lifecycle. Suppose, you were viewing a particular TODO UI from a list. It has its own screen. You have a few tabs for a TODO, like comments, notes, likes, etc. You always want the TODO to open with comments tab selected. So, on componentDidUnmount of the TODO, you will reset the tab selection to comments.
We have discussed representation and user interaction which is controlled by React. What about the other important things in our application?
The answer is Redux.
It has 3 parts: Actions, Reducers, and Store. I have put the concerns into 2 groups:
- Brain — This will use only the Actions part of Redux
- Storage — This will use Reducers and Store
So, the application data is maintained in redux store and representation in React. Everything else which will be a part of your application will come in the “brain”. It will decide how your application functions and what representation to be shown.
Everything which will be a part of a brain will be written in ACTIONS.
Here are the concerns which will be a part of the Brain. For each of these, there will be an entity which will take care of addressing that concern. For example, there will be a different class/file for handling routes like routeHandlers, and for validations like routeValidator.
This comprises a model which has the logic/algorithm for determining the state of our application. It can include anything from simple algebraic operations to a set of steps for performing a particular task.
For example, let’s take a quiz app. At the beginning of a quiz, the following things should happen:
- show loading fetch quiz data
- start quiz timer
- remove loading
- load questions
This is the algorithm which will be followed to start to a quiz.
This will contain all the things which should happen after you hit any route.
- Check if the route is valid —For example: if someones hits a route /todo/18. We’ll have to validate if a TODO with id 18 exists or not. If not, then take necessary action
- Check if the user has permission to hit that route
- Reroute if required
- Check if enough data is available for that route, if not then fetch the data
All the AJAX requests will come here. This will comprise:
- An algorithm which should be followed by all AJAX requests.
- Error management or internet connectivity issues.
- Synchronizing or sending parallel multiple API calls.
- Grouping multiple APIs so that it can be reused.
This comprises all types of authentications which will be a part of your app:
- user authorization
- for seeing a particular UI
- for taking a particular action
- for hitting a particular route
- user input data
- data from AJAX requests
Controlling component’s state
- Component’s state can be changed/controlled based on the action
- Suppose we want to perform a heavy operation which might take a few seconds. We should have a control to change the component’s state to ‘waiting’ so that it can show some loader in the UI until the operation is completed.
- This will contain reducers (pure functions) and a single object called store which will save your application state
- The only way to change its state is by dispatching an action. It’ll update depending upon the action type and the data passed with the action
- The Store will trigger events when its state is updated.
- The view can subscribe to those events and update itself accordingly.
- We should use React because it lets you write code in JSX. It provides code re-usability and lets you control the unnecessary rendering of components.
- Use Redux because it provides a “unidirectional data flow.” This helps to manage and organize data better and makes debugging a lot easier.
- React should only render HTML content, have multiple UI states, dispatch actions on user interaction, and perform animations.
- React shouldn’t request data if it’s not present. It shouldn’t dispatch actions in a component’s life cycle events if the action is not directly related to the life cycle event.
- Redux has 3 parts: Actions, Reducers, and Store.
Redux will take care of these 2 concerns:
Brain — This will use only the Actions part of Redux
Storage — This will use Reducers and Store
- Brain comprises functionalities which define your application.
Controlling component’s state
- Storage will contain reducers (pure functions) and a single object called store which will save your application state. The only way to change its state is by dispatching an action.
Now, when you have decided on the roles/responsibilities of react and redux, you can architect them individually.