Redux based architecture can be dangerous

Matters (@matterstech) is a startup studio building the alternative products of tomorrow. For Matters, it is key to be able to scale fast our development team when one of our products get good feedback on the market. Frontend is a particular area where iterating was always quite painful. Redux is a technical lib used in frontend development. It is a simple design pattern, similar to CQRS which helps preventing some specific types of bugs.

There is plenty of articles about all the Redux qualities, and if you are reading this article, you probably know a few already :). For us, it was mainly a good way to refactor and test several UIs without having to constantly refactor controllers, async code, and data bindings.

So is everything perfect with Redux? Not quite, as Redux is based on the “exotic” functional programming approach, it may be wrongly used. This article targets developers planing to use Redux at scale.

We also made a talk in the ReactJs meetup in San Francisco about the topic, and it was recorded:

We deployed our first Redux-based frontend in 2016 with 120 React components and 128 Redux action types. Based on this success, we decided to deploy 4 other large apps with Redux, all with more than 100+ action types. Overall 25 developers interact daily with Redux at Matters.

Here is what we learned scaling those apps.

1/ Plan 1 day of training per developer dedicated to Redux

We discovered too late in the project that some team members got misled in what Redux is. Redux is not natural or intuitive. A developer coming from a Js background is not necessarily versed in functional programming or immutability.

The best way to debunk those preconceived ideas is to read the official doc. Now I make sure that everyone books a full day to read the official documentation from start to finish before jumping in the code.

2/ Integrate Redux-dev-tools as early as possible

Redux-dev-tools is an awesome Chrome and Firefox extension which provides a great value for any developer using Redux. Not only it helps understanding Redux when you learn, but it also helps us everyday correcting bugs in production.

It will help the whole team taming the Redux beast; make sure everybody knows how and when to use it.

Usually it is sufficient to just add this middleware:

Integrating Redux-dev-tools

3/ Use action creators

Action creators provided us quite a lot of value for the time they took to put in place. At first, they seemed to me like a superfluous abstract layer. Oh boy I was wrong.

Action creators look like this:

Example of action-creators

What are the advantages?

There is an explicit dependency between the component dispatching the action and the action

There is an explicit API to interact with the state

It provides a few really elegant shortcuts when binding those actions to React component

Check this out:

Action creators make your life easier when connecting to React

The “connect” function from react-redux indeed accepts directly a collection of action creators in its interface, making the code super readable.

It is just one of the several examples of 3rd party APIs making your life easier with action creators.

4/ Organize your file system with Redux Ducks

Redux is a library. It doesn’t enforce any specific organization of the source code. When our dev team got larger, we felt the need for more structure, and redux-ducks was just that. Redux-ducks is a convention and is not a library. It defines a standard interface for Redux-related module.

Here is the gist of it:

Redux-ducks, a sane standard for Redux module organization

We really liked that it enforces action creators everywhere. It also reduced the number of dependencies and Redux files by grouping in the same file what is related together.

Overall, it was a sane convention, which perfectly sorted all of our Redux file hierarchy.

5/ Write unit tests, it’s *free*

What makes unit testing hard in a procedural or OOP code is isolating the “unit”, as well as setting a particular initial state.

Redux-based code uses a functional programming approach, and is built by composing pure functions. Pure functions are functions which output the same result if called with the same inputs. You could call them deterministic, side-effect-free etc.

As pure functions have no internal state, setting the initial state for unit testing is just a matter of setting function arguments

Each element is already an isolated function before composition.

Unit testing Redux is easy

We used Jest with state snapshot to go fast. Quite a lot of people prefer to test explicitly parts of the state, but snapshotting have been very sufficient for the 6 months of the project. We caught all the regressions and could refactor confidently. And for the first time, we had maintained unit tests with 100% coverage of the Redux code base.

6/ Make general APIs using payload-based action creators

We have two ways to edit users: their name, or their email address.

At first we made two different action creators:

users/editName(name) and

users/editEmail(email).

We never used the two different types of actions, the reducers were quite similar and we ended up duplicating a lot of code. Worse, we couldn’t easily add a field to the user object without duplicating a bunch of code.

We decided later on to merge back those action creators into a single:

users/edit(payload).

It has been sufficient. Keep it simple.

Oh last point about that: just pack what may be useful in the action, don’t go thinking an action is a “command” or a “RPC” call which should have tailored parameters. Redux actions are “facts”, if I add a user, I put in the action everything I know about this new user, the reducer will pick what it needs.

7/ Anticipate that you will have a lot of async-handling code

One error I saw in a few other projects I audited was to assume that async and side-effects code is a secondary problem and solves it with redux-thunk, which is the lowest barrier of entry. The biggest problem in my opinion with Redux thunk is that it propagates the dispatch function everywhere. That makes it way harder to figure out who dispatches an action, as suddenly regular action creators may not be side-effect free.

Redux-saga was a better bet for us. It provides high level abstraction and patterns, like debounces, retries or throttles. It leaves actions as plain objects easier to unit test, and isolate all the side-effects in nice looking sagas, leaving your Redux code pristine.

For instance, if you plan one day to have an offline mode, I highly recommend you to onboard from the start something more advanced like redux-saga. Refactoring this part later is a real pain.

8/ Normalize your state like a database

It seems obvious after you say it, but avoid duplicating information. Make sure to have only one reference to each object. For us, we followed best practices that already exist in the database world.

If you don’t know how to do it, it is probably the moment where you sit down with one person from the backoffice to get this straight, as a db guy will be able to do this task in no time.

Normalizing a Redux state

Based on this example, retrieving one user and its groups become slightly more complex, which leads us to our next tip.

9/ Use selectors

Selectors are convenient functions returning parts of the state. If you normalized your state, there is huge chances that your state won’t be perfectly aligned in format with what your components consume.

For instance, with the example above, imagine we want to display a list of users and their groups by name.

It keeps your components independent from your state shape, making it possible to use straight out of the can some Material, Bootstrap or generic components. Bonus, you can use a lib like reselect to really speed up this transformations as well.

10/ Flowtype, Reasonml or Typescript can really help

This advice goes beyond simply the scope of this project, but types particularly help with state management. I know 3 ways to do that in JS:

migrate to a superset of js like Typescript

adopt another language like Reasonml

retrofit your js codebase with Flowtype and add type annotations as you go

Adding just one line with //@flow at the beginning of our redux ducks modules caught a lot of missing cases without even going to the type annotation route.

It’s free and it’s worth it.

Conclusion

I hope all those advices will help you structuring a large Redux codebase.

We are very interested in hearing your point of view, you are welcome to share your opinion in the comment section or on Twitter @batmansmk.

Matters is a startup studio building the alternative products of tomorrow. It is based in Paris and San Francisco. Tweet us @matterstech or send a cute email hello@matters.tech for more details.