In this series we are looking at code organization in the context of a React and Redux application. The takeaways for the “Three Rules” presented here should be applicable to any application, not just React/Redux.
Series contents
- Part 1 - Three Rules for Structuring (Redux) Applications
- Part 2 - The Anatomy of a React & Redux Module (Applying The Three Rules)
- Part 3 - Additional Guidelines For (Redux) Project Structure
In this post, we are expanding on the three rules of structuring applications and diving deeper into the contents of the different files with a Redux and React application.
This post will be specifically about React and Redux, so feel free to skip to the next post if you are not interested in these libraries.
In-depth example and recommendations
Recall the three rules presented in the previous post:
- Organize by features
- Create strict module boundaries
- Avoid circular dependencies
Now, let’s take a look at our TODO app again. (I added constants, model, and selectors into this example)
We can break these modules down by their responsibilities.
Module index and constants
The module index is responsible for maintaining its public API. This is the exposed surface where modules can interface with each other.
A minimum Redux + React application should be something like this.
Actions & Action creators
Action types are just string constants within Redux. The only thing I’ve changed here is that I prefixed each type with “todos/” in order to create a namespace for the module. This helps to avoid name collisions with other modules in the application.
As for action creators, not much changes from the usual Redux application.
Note that I don’t necessarily need to use addTodo
since I’m already in the todos
module. In other
modules I may use an action creator as follows.
Model
The model.js
file is where I like to keep things that are related to the module’s state.
This is especially useful if you are using TypeScript or Flow.
Reducers
For the reducers, each module should maintain their own state as before. However, there is one particular coupling that should be solved. That is, a module’s reducer does not usually get to choose where it is mounted in the overall application state atom.
This is problematic, because it means our module selectors (which we will cover next) will be indirectly coupled to the root reducer. In turn, the module components will also be coupled to the root reducer.
We can solve this issue by giving control to the todos
module on where it should be mounted
in the state atom.
This removes the coupling between our todos
module and root reducer. Of course, you don’t
have to do it this way. Other options include relying on naming conventions (e.g. todos
module
state is mounted under “todos” key in the state atom), or you can use module factory functions
instead of relying on a static key.
And the reducer would look as follows.
Selectors
Selectors provide a way to query data from the module state. While they are not normally named as such in a Redux project, they are always present.
The first argument of connect
is a selector in that it selects values out of
the state atom, and returns an object representing a component’s props.
I would urge that common selectors by placed in the selectors.js
file so they
can not only be reused within the module, but potentially be used by other modules
in the application.
I highly recommend that you check out reselect
as it provides a way to build composable selectors that are automatically memoized.
Components
And lastly, we have our React components. I encourage you to use shared selectors here as much as possible. It gives you the advantage of having an easy way to unit test the mapping of state to props without relying on component tests.
Here’s an example of a TODO list component.
Other responsibilities
There are many other things that could be included in a module. For example, you may be using redux-saga, which should definitely be included in your module structure – where each module can optionally expose a root saga.
Other things that you may include are route handlers (or containers) for use with react-router – or a similar routing library.
Summary
We’ve seen in this post how to break down a React/Redux module into individual core responsibilities.
- Index and constants - The public API and constants.
- Actions and action creators - Information that flow through the application.
- Model - Types and utilities for the model.
- Reducers - Updates module state.
- Selectors - Queries module state.
In the last post of this series on code organization, we will discuss additional guidelines that fall outside of the “Three Rules.”