< Back to articles

Data Dependencies Management in Redux Based Applications 2/2

The data dependencies management is an important topic for me. Over the years, while working with React, Redux and other libraries from this ecosystem, we developed a few solutions for this problem. Here is our story.

In the previous article I have shared with you some basic information about the technologies that we use to create React applications, about how we separate concerns using React, Redux and redux-saga. In this article, I will tell you how we manage the data dependencies in our applications. By data dependencies I mean data an application has to request from some service. Let's be specific and say the service we are using is REST API, but it can be any other service as well. By data I mean any data an application needs or has.

Presentational and Container Components

I have to start with an explanation of the React component structure we use. Our applications include two types of React components: presentational and container components. Presentational components are, most of the time, stateless components accepting data via props and rendering markup. They only know how to render UI. Container components, on the other hand, know about the business logic and usually wrap presentational components with some HOCs (like react-redux connect etc.). If you want to know more about the concept, you can find the source article here.

So, the first thing we absolutely need to do is to inject data into presentational components using container components. And in our Redux applications, data are taken from the Redux store via HOC connect. But how to get these dependencies into the store from the REST API?

Using Container Components

As I have mentioned above, the container components know about the business logic. They know which data dependencies they need. Why not use these components to request the data dependencies then? We can use React lifecycle:

  • componentDidMount: when the component mounts, it asks the REST API for the data,
  • componentDidUpdate: when the component updates, it can optionally ask for the new data,
  • and componentWillUnmount: when the component unmounts, it can delete the data it doesn't need.

Usually we just dispatch Redux actions in these lifecycle methods. The actions express “I want this and this kind of data” or “I don't need this data anymore”.

Deleting Data

As far as deleting data is concerned, we don't need anything more than Redux. You need a reducer function that listens to a given action and deletes the data from the store.

Requesting Data

Requesting the data dependencies starts to be a bit more complex, but just a little. As I mentioned in the previous article, we use redux-saga for handling the business logic. So there has to be a saga (we call a generator function for the saga middleware a saga) listening for the action asking for the data. This saga then calls the REST API and subsequently dispatches a new action to store the data dependencies in the Redux store.

That is all you have to do to get the data you need for the component. When the container component is mounted, it requests the data dependencies via some saga, and when it is being unmounted, it deletes the data it doesn’t need anymore.

Routing Data Dependencies

The idea I have shown to you above is simple but the solution can end up being pretty complex. Imagine a situation when you have multiple components with the same data dependencies rendered simultaneously. Each of the components requests the data dependencies, each of the components deletes data. You have to synchronize the actions somehow and even then you end up with invocations of code you don't need. It would be enough for a given page of the application to ask for and delete the data only once.

Page Content

Let's say you have a component that renders a content for a given page. We can call this component a page container. The page container could ask for all the data you need in the page content and then delete the data when the page is changed. The other components in the page content will just render the data from the store. They will not ask for the data themselves, the page container will do it for them.

This idea goes back to the times when every page was rendered at the server and the data were injected for a given page at the server render time. To make it all work in React application, you just need react-router.

Change of Mindset

Let's look at the situation from another point of view. Do we really need to do all this asking for the data logic in some component? It would be nicer to move all the business logic (which asking the data is) into redux-saga. Lucky for us, there is connected-react-router package that inserts information about a current route into the Redux store. Every time a route changes, LOCATION_CHANGE action is dispatched. We can listen to this action and based on the current route ask for the data that all components in the page's content need. With this change of mindset , we can simplify the data dependencies management and even support better separation of concerns.

RouteDependenciesSaga

So I wrote something we call runRouteDependencies. It is a utility to help you manage the data dependencies in your application. I believe it can be best explained through an example, here it is:

export function* userDetailSaga(params) {  
  
 yield put(requestUser(params.id));  
  
}  
  
export const handlers = {  
  
 '/user/:id': userDetailSaga,  
  
};  
  
export function* routeDependenciesSaga() {  
  
 yield takeEvery(LOCATION_CHANGE, runRouteDependencies, handlers);  
  
}  

routeDependenciesSaga is a saga to manage your data dependencies. It listens to every LOCATION_CHANGE and calls runRouteDependencies with handlers object. It calls a specified saga to request all the data dependencies based on the current route (a user detail in the example above).

Note: The code in the example just dispatches a new action requestUser with a user id. You need another saga to perform an API call.

Moving Back to the Container Components

Using routeDependenciesSaga seemed to be the way to go. Having the data dependencies management in one file was easy to debug as well as to read and understand. When you needed to change something or add some new dependencies, you just had to find the correct file. But there are some cons to this approach.

routeDependenciesSaga Problems

Authentication Problem

Imagine you have an application with some routes that are under authentication. When a user comes to the route, you have to show a login form unless the user logs in. The LOCATION_CHANGE is dispatched and the data dependencies are requested. But you need, for example, some user token to access the data. So the request fails because the user is not logged in. This leads to more complex routeDependenciesSaga than the one above. You need to know a lot about the authentication in the routeDependenciesSaga to make it work properly.

Data Deletion Problem

Another problem is the deletion of data from the store when you leave a route. LOCATION_CHANGE tells you the current route. Therefore you know that you have entered the route and you should request the data. But you never know which route you have just left. When you are creating some complex application with a lot of data dependencies, this can cause performance issues.

Reusability Problem

And there is another problem with the reusability of components. If you want to use some component from one page on another page, you have to modify its dependencies handler too. You can't simply just use the component.

How to Solve the Problems

Is it really worth it to have all the data dependencies management in redux-saga? We had some arguments about it with the guys from my development team but it seems that I have managed to explain to them the benefits of moving back to the container components.

I'm not saying we are not using redux-saga anymore. The data dependencies are still requested via sagas. But we don't use routeDependenciesSaga. We have the container components asking for the data. This solves the authentication problem. Because the page container component is not rendered when the user is not logged in, it won’t ask for the data dependencies. So the request won’t fail. When it is rendered, you can be sure the user is logged in and can request the data.

The solution to the data deletion problem comes with the container components as well. When the component is being unmounted, it can dispatch an action to tell Redux to delete the data it doesn't need anymore.

And if you rethink the page container idea a bit, you don't have to use it just for whole pages, you can use it for parts of pages and when reusing some components, you can even use it for a single component. The idea is to have just one component on the page asking for the specific kind of data. This solves the reusability problem.

fetchDependencies High Order Component

So we have created fetchDependencies HOC to reuse the idea of the page container component. You can wrap your components with it and tell it to ask for the data dependencies on mount and delete the data on unmount. It supports re-requesting data when the route (or other props) is changed. Here is an example of the usage:

export const UserContainer = compose(  
  
 connect(  
  
    (states) => ({  
  
       showLoader: selectUserPending(state)  
  
    }),  
  
    dispatch => ({  
  
       askForData: id => {  
  
          dispatch(getUser(id));  
  
       },  
  
       deleteData: id => {  
  
          dispatch(clearUser(id));  
  
       },  
  
    }),  
  
 ),  
  
 fetchDependencies({  
  
    onLoad: ({ id, askForData }) => {  
  
       askForData(id);  
  
    },  
  
    onUnload: ({ id, deleteData }) => {  
  
       deleteData(id);  
  
    },  
  
    shouldReFetch: (oldProps, newProps) =>  
  
        (oldProps.id !== newProps.id),  
  
 }),  
  
 loadable,  
  
)(User);  

As you can see, the container knows nothing about the current route. All it does is ask for the data on component mount and delete the data on component unmount. It re-asks for the data when props change.

Adding React-Router Context

There is another HOC called routeDependencies. The HOC adds an access to the react-router context so you can manage your data when a route changes. This is really useful when you use a query string to store app state or when using react-router@4 with its dynamic routing.

Conclusion

This is basically the way we manage the data dependencies in our applications now. We use presentational/container component pattern for React components, we store the data in Redux store, we use sagas for business logic, and we use fetchDependencies and routeDependencies HOCs to ask for the data and to delete the data we don't need.

The article can be a bit confusing. I've shown you something about the container components, then have switched to sagas, and then back to the containers but in a little more detail. However, it is exactly the way we have developed the current solution with the HOCs. It feels like the solution is the best one we have found so far. It encourages the usage of the abilities React gives you to simplify your business logic and gives you an easy way to write reusable components. And it still encourages the separation of concerns as well.

PS: While writing the article, an idea came to me. It is not so clear how to store data in the Redux store and how to get them for displaying in the presentational components. My next article will be about Redux data management.

PPS: If you are interested in the implementation of both HOCs, check out our npm package @ackee/chris (fetchDependencies, routeDependencies)!

PPPS: the HOCs are written so they can be easily rewritten into React Hooks! I’m so looking forward to rewriting it).

Marek Janča
Marek Janča
Frontend Developer

Are you interested in working together? Let’s discuss it in person!