My journey toward a maintainable project structure for React/Redux

Written by mmazzarolo | Published 2016/10/03
Tech Story Tags: redux | react | project-structure

TLDRvia the TL;DR App

When I started learning Redux I was shocked by the number of discussions and “best practice” you could find online about it, but it didn’t take too much time to understand why: Redux is not very opinionated about the way of structuring a project around it, and this can lead to some annoyance when you’re trying figure out what kind of structure suits better your style and your project.

In this post I’d like to share some information on my journey to achieving a comfortable Redux project structure.

This is not an introduction/tutorial, a bit of knowledge of Redux is required to understand it entirely.

Also, apart from redux-sagas (which can be replaced by redux-thunk or by your favorite library for handling asynchronous actions), I won’t use any external Redux library/utility.

Hope you find it interesting!

First stop: grouping files by “type”

When I started using Redux I studied the official doc from top to bottom and I organized my project this way:

This structure is promoted by the official Redux repository examples and in my opinion it is still a pretty solid option.The main drawback of a structure like is that even adding a small feature might end up in editing several different files.For example adding a field (that is updated by an action) to the product store means that you’ll have to:

  • add the action type in types/product.js
  • add the action creator in actions/products.js
  • add the field in reducers/product.js

And it doesn’t end here! When the app grows you’ll probably add other directories to the mix:

So, after introducing redux-saga in my project I relized that it was becoming too hard to maintain and I started looking for alternatives.

A different approach: grouping files by feature

An alternative to the project structure above consists in grouping files by feature:

This approach has been promoted by various interesting articles in the React community, and it is used in one of the most common React boilerplate.

At first glance this structure seemed reasonable to me because I was encapsulating a component (the container), its state (the store) and its behavior (the actions) in a single folder, following the React’s component concept.After using it in a bigger project though I discovered that it is not all sunshine and rainbows: if you’re using Redux you’re probably doing it for sharing a slice of store across you’re app… and you can see easily that this clashes with the encapsulation concept promoted by this structure.For example dispatching an action from the product container might produce side effects on the cart container (if the cart reducer reacts in some way to the action).

You must also be careful not to be caught in another conceptual trap: don’t feel forced to tie a slice of the Redux store to a container, because otherwise you’ll probably end up using Redux even when you should have opted for the simple setState.

Ducks to the rescue

After the small adventure of grouping by feature I went back to my initial project structure and I noticed that using Sagas for handling the asynchronous flow has the interesting side effect of turning 90% of your action creators in one-liner:

const login = (email, password) => ({ type: LOGIN_REQUEST, email, password })

In a situation like this, having a dedicated file for each action creator, action type and reducer seemed a bit overkill, so I decided to try out the “Ducks” approach.

Ducks is a proposal for bundling reducers, action types and actions in the same file, leading to a reduced boilerplate:

The first time I adopted this syntax I fell in love with it.It's clean, it removes a lot of unnecessary boilerplate and you can easily add an action or a field to the reducer by changing a single file.

Unfortunately though, using ducks started showing quickly its limits because exporting individually every single action creator and action type has some nasty side effects.

  • In bigger containers you’ll end up having a huge list of imported actions that are only used one time (for feeding mapDispatchToProps):

import { signup, login, resetPassword, logout, … } from ‘ducks/authReducer’

  • You won’t be able to pass to mapDispatchToProps directly all the actions of a duck. Using import * as actions won’t work, because you’ll end up importing the reducer too.
  • You’ll waste a super precious variable name just for passing it to mapDispatchToProps.You won’t even be able do something like const { login } = this.props because you already defined the login variable by assigning it to the action creator imported from the duck.

  • In bigger sagas you’ll end up using a lot of different actions without knowing their context (you’ll have to scroll to the top imports every time).

Customizing the ducks

My solution to the above issues is simple: instead of exporting individually action types and action creators I group and export them inside a types and actions object:

If you structure the ducks this way you’ll be able to import the actions easily inside your components:

Now you may ask: what if I need to dispatch actions of different ducks inside a component?Well, you can do something like this:

I know, it’s a bit uglier, any alternative solution is welcome!

Originally I opted for the name actionTypes/actionCreators instead of types/actions, but after a bit I refactored to the latter: the first option was too verbose for my taste.

P.S.: From now on, for simplicity, I’ll keep referencing the files containing actions/reducers/types as “ducks”, even if in my current projects I have them in the “reducers” folder.

Selectors

In my opinion selectors are the most overlooked feature of Redux.I must admit that I started used them a bit too late, but reading this tweet of Dan Abramov opened my eyes and I begun viewing selectors as interfaces that expose the store to the containers.

  • Need to display a list in a certain order? Define the getProductOrderedByNameselector selector.
  • Need to get a specific element of a list? Define the getProductById selector.
  • Need to filter a list? Define the getExpiredProducts selector.

By following this strategy most of the selectors you define will be strongly tied to a specific reducer, so the right place for defining them is the file containing the reducer itself).

Sometimes you’ll need to define more complex selectors that handle the input from different slice of the store.In this situations I put them in reducers/index.js.

The sagas

Sagas are really powerful and testable, but being used for managing asynchronous actions and side effects make it a bit hard to reason on how to add them to your project structure.My suggestion is to start grouping sagas that are triggered by a single redux action in the same action domain.This means that if you have a reducer that handles the authentication in ducks/auth.js, you can create sagas/auth.js, containing the sagas that are triggered by authTypes.SIGNUP_REQUEST, authTypes.LOGIN_REQUEST and so on…

Sometimes thought you’ll need to trigger the same saga from with different actions. In this case you can create a more generic file containing this kind of sagas. For example this simple saga for React Native shows an alert when it intercepts an error:

If the generic file (in this case i called it sagas/ui.js) grows too much you can always refactor it later being more specific.

Another thing worth nothing is that in sagas/index.js I have a file that links the take…instruction of every saga to its implementation:

In this way I’m able to track down every saga easily by associating the saga with action types that trigger it.

Conclusion

That’s it! Here is my current project structure (as I anticipated above I renamed the “ducks” folder to “reducers”):

I know there’s still room for improvements, and this is probably the main reason that made me publish this post: tips and critics are welcome!

Edit #1: Where do you place actions/types that are not tied to a specific reducer?

In most of my project I have one (or more) generic duck that handles this kind of actions. For example if the project is small enough I simply create ducks/app.js, a duck file containing 1) all the actions creators and action types that are not reducer specific and 2) the state of the ui/app:

Please keep in mind that these ducks may not need a reducer: they can contains just action creators and action types.

Edit #2 A more explicit approach

Some comments on Reddit and Twitter hinted that ducks may promote the idea that the relation between actions and reducers is many:1, when it actually is many:many. I agree with them. In fact (in my opinion) a structure that follows the Redux philosophy even more than the one I showed you would be the following:

I still prefer to use the customized ducks though, and when I have actions that are not tied to a reducer (or the opposite) I adopt the solution I explained above in the edit #1.In the end you might need to test different structures by yourself for finding the tradeoff between explicitness and comfort that suits better your style and your project.

Thanks to @mxstbr and joshwcomeau.

Credits and useful links

  • React/Redux Links list by Mark Erikson
  • This discussion in a pull request of the awesome Ignite boilerplate repository (which uses a structure just like the one I showed you)
  • Steve Kellock, because he inspired me to write this post and because we had an interesting discussion about Redux that you can find here
  • Dan Abramov, because all the stuff he posts is super-interesting and well written (and thanks for Redux too! :P)
  • This baby boilerplate, a bit outdated but I’ll try update it as soon as possible

Hacker Noon is how hackers start their afternoons. We’re a part of the @AMIfamily. We are now accepting submissions and happy to discuss advertising &sponsorship opportunities.

To learn more, read our about page, like/message us on Facebook, or simply, tweet/DM @HackerNoon.

If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!

https://upscri.be/hackernoon/


Published by HackerNoon on 2016/10/03