Lead Developer (Fullstack)
React+Redux stack has become a very popular choice for building modern web applications. There‘s more than enough of good tutorials about them, but I haven’t found one explaining what to do after ToDo-app is completed.
In this post I’ll explain how I’ve managed to scale my projects both horizontally (adjacent features) and vertically (complex features).
Before we’ll dive into code examples, let’s agree on some naming conventions. For me naming things has always been hard and felt opinionated so please take this with a grain of salt.
With Redux, I’ve tried organising my action creators and reducers in few different ways and now I’ve ended up using Erik Rasmussen’s proposal of splitting Redux applications into reducer bundles (a.k.a ducks). I like the idea but not so much the name, so I’ll be talking about concepts instead of ducks (might as well be services for that matter). The point of them is to create as cohesive and reusable code modules as possible to build our application on top of.
For the sake of example, imagine that we’re implementing an application that displays some kind of listing of its users. Let’s start with a simple concept that fetches users from the API and stores them into Redux store.
This concept is then used by a container component to render the fetched data.
This kinda works, but I don’t like the idea of internal structure of my store leaking into containers (I’m looking at you,
mapToState function). If I want to rename, for example
isLoading in store, I’ll have to make changes in every single container using this concept. And since we’re talking about scaling things for more complex applications, this won’t do.
An easy way to fix aforementioned problem is to implement an API for accessing concept’s store using selectors. They could be called getters for that matter, but I like to use Reselect library since it provides memoized results and selector composition, so the naming kinda stuck from there.
So, let’s add some selectors into the concept.
And also update also the container:
Now our container doesn’t have to know about the internal representation of the store and we also have more testable and reusable API for accessing part of our application state.
In my experience structuring your code like this works rather well with simpler CRUD-type applications when most of the connected containers will only depend on single concepts.
The limitations of this approach begin to appear when we start adding more complex features that often mean data aggregation and/or orchestrating multiple actions into application logic.
Our end-users have been satisfied with our application, but now they want more. They’ve requested to see number of alerts per user and some kind of user activity score in the user listing.
The API we’re using doesn’t provide this information out-of-the-box so we’ll have to make multiple requests to get all the information we need.
We could refactor
fetchUsers() to make the other requests too, but that tightly couples loading users, alerts and activity score together. If we’d like to fetch only user information (or aggregate it with something else), we’d have to add more parameters and/or conditions to action creator and reducer.
One symptom might be the increase of number of
componentWillReceiveProps() functions in React components where you try to deduct from the current and next props what has actually happened.
We could also implement this logic in container components. One way of doing this would be to implement connected containers as higher order components that only wraps the presentational components and don’t render anything by them self. This is already better, but I’d say it still has few shortcomings; we could make unit testing bit easier and we still need a good place for storing view specific data (i.e. search terms and such) in store somewhere.
Recently I’ve started organising my concepts into two different categories: basic concepts and container concepts.
The former are pretty much like the one we’ve already wrote. They are the basic building blocks of our application which means they…
In our example we’d might have separate
activityScores concepts. This might sound a bit of over-engineering and at this point it is that, but try to keep in mind that we’ll be adding more and more new features to those concepts in the future.
The container concepts on the other hand are almost the opposite. They…
So let’s see how those container concepts could look like and let’s create one that handles fetching and aggregating the users, alerts and activity scores data.
Since our user list specific concept doesn’t have any state of its own, we don’t need a reducer for it. If we would like add support for, let’s say, filtering by search term, search term could be added to this concept’s store since it can be seen as a view/container specific concern.
The action creator and the selectors are more interesting part, though.
fetchUsers() action creator now encapsulates the logic of making multiple requests to different endpoints by making use of action creators provided by other concepts. It doesn’t need to know the details of how or where the data is exactly retrieved. Its only concern is to make sure that required information will be fetched when needed.
Sometimes I just only re-export the action creators from other concepts without adding any wrapping to them. I prefer this approach to depending on multiple concepts in containers.
createSelector() function from Reselect to combine and reformat the data to be more suitable for container. As an example we are calculating min, max and average values for user’s activity score for the past 7 days. Notice that we also convert key-value data to a list sorted by username so it’ll be more easier to render by components.
Because Reselect offers us memoization of results, we don’t have to worry so much about the performance impact either.
So far we’ve managed to keep the basic concepts simple, independent and easy to test. Application logic hasn’t leaked into React components meaning that it’s easy to unit test too. Also its 1-to-1 relationship with container means that we don’t have to worry about breaking something, somewhere else if and when we make modifications to container concept. It also nice to notice that the API used by the container is still the same from where we started.
For people like me who have been using MVVM pattern this should be nothing new. Basic concepts are the model, container concepts are the view-model and containers are the view.
In real-life there are some limitations with this approach too. In some cases we have noticed that this two-tier approach for concepts is not quite enough, but we’ve solved this by adding more refined basic concepts that depend on concepts below them. Just make sure that there’s a clear hierarchy in your concepts’ dependency tree.
There’s also no clear line between general and container specific APIs. We keep our concepts in the same directory, but it would also make sense to make clear distinction between them by keeping them in different directories to help your thought process. I’d recommend going with the solution that feels the best for you.
My current work project is dashboard-like application (over 60kLOC) with lot of data aggregation and data refining, and complex application logic. Structuring our code in this manner has allowed us to easily scale both vertically and horizontally. For us that means less regression which means happier customers which eventually means happier coders.
I’d love to hear your thoughts about structuring your code in this manner? What kinda solutions have you used in your projects and how have they worked for you?
Create your free account to unlock your custom reading experience.