Angular + Redux — The lesson we’ve learned for you
In my first blog post I’d like to share our experiences with Redux and Angular in hope of helping out the ones who are just about to onboard with this stack. It can save you a lot of headaches if you’re familiar with Angular but Redux is completely new to you.
Back in late 2016 when Angular 2.0 was finally released, we were already playing with rc5 and managed to release our first app in production. Although I was impressed by Angular’s two-way data binding and the capabilities of RxJS, the two-way data binding and the principle of “we-are-so-reactive-everything-is-bound-to-everything” create a degree of dependence that fails in terms of maintainability and code readability. In addition, it was very hard to track down update loops or just to find a simple bug. I was sure that we were doing something wrong and the basic Angular setup is supposed to work.
So I did some research on the topic. I found a couple of articles that recommend to use Rx streams in services as a single source of truth. It can solve the “complete graph”-like connections issue within components and services. It was in line with our Android mobile team’s advice: consider managing your app’s state in a centralised data repository by implementing the Flux architecture.
I also found the Redux library, but like so many other developers at the time, that name was fused in my mind with React, so I thought, “Good, good, except we’re using Angular.” I stuck to this mindset until I went to the AngularUP conference in Tel-Aviv, where one of the talks was literally called Angular+Redux, and explained in detail how the two frameworks can be brought together. I decided to give it a try after all.
(I’d like to add, merely to save some face, that this concept was pretty novel back then, as Angular 2.0 had just celebrated its second month around the time the conference took place.)
If you’re not yet familiar with Flux and Redux or just need a short recap, don’t skip the following part. Otherwise you can jump to Angular-redux.
Short introduction into Flux architecture
React had been on the market for about a year when Facebook announced the “Flux Architecture“:
“We found that two-way data bindings led to cascading updates, where changing one object led to another object changing, which could also trigger more updates. As applications grew, these cascading updates made it very difficult to predict what would change as the result of one user interaction. When updates can only change data within a single round, the system as a whole becomes more predictable.” — https://facebook.github.io/flux/docs/in-depth-overview.html
Let’s say the application state is global and the components’ states are local. Flux uses stores to hold global (application) state. It means that any component, anywhere in the app has access to this data repository. This is the single source of truth. Local states are not allowed to hold any data that could be needed by any other component. The store can be changed through actions only. Actions are dispatched by user interactions typically. Views’ (local) state depend on what they get from the store, and that’s how the circle closes:
A Flux implementation: Redux
According to the official site of Redux:
“…Redux attempts to make state mutations predictable by imposing certain restrictions on how and when updates can happen. These restrictions are reflected in the three principles of Redux.” — https://redux.js.org/introduction/motivation
1. Single source of truth
“The state of your whole application is stored in an object tree within a single store.”
This enables developers to, among others:
- easily inspect / debug the application’s state.
- save and load a preset state for faster development by jumping to the point where development is taking place.
- handle traditionally difficult functionalities corresponding to app state like undo/redo.
- attach a state snapshot to error reports.
2. State is read-only
“The only way to change the state is to emit an action, an object describing what happened.
Because all changes are centralized and happen one by one in a strict order, there are no subtle race conditions to watch out for. As actions are just plain objects, they can be logged, serialized, stored, and later replayed for debugging or testing purposes.”
Another convenient feature of Redux is time traveling. Since the store can be mutated by actions only, the state change is predictable. If your app is completely stateless (no components can exist on their own), you can rewind or replay the history by reapplying the recorded actions one-by-one, always starting from the “default state”, and you are sure to end up in the same app state. You can even change the history by reordering or skipping some actions.
Check out this blogpost if you are interested in details.
3. Changes are made with pure functions
To update the state, you write reducers. A reducer receives an action (one at a time) and the current application state and returns the updated state (modified or not), and does nothing else. Reducers are just pure functions, so you can combine them, which enables you to break down the store update logic to small chunks and merge at the end. This helps developers write maintainable code.
There were two popular libraries on the market at the time, which are connected to Redux in some way:
@angular-redux (aka ng2-redux) uses Angular’s features to help integrate the Redux library painlessly.
@ngrx is inspired by Redux but a completely new implementation. Redux’s source code is not included.
From a developer’s point of view, the two libraries work the same way. I decided to give Angular-redux a try, based on the below:
- Redux was already a popular library, which guarantees community support.
- Redux had been stable for a while.
- Really cool devtools (like redux-devtools, redux-logger).
- I’ve seen it working on stage :) (AngularUP conference)
We all went through the angular-redux tutorial / documentation, but it wasn’t as detailed at that time as it is today. We tried to cherry-pick the best practices from the available angular-redux documentations/tutorials/articles, but there are always things you cannot forecast. We made mistakes; nonetheless, the app was shipped on time and is running well. Here’s what we could highlight from that experience:
Everything’s stored in redux store
This wasn’t the best idea. We stored the form’s state, the navbar’s open/closed state, the drawers’ open/closed state, and even animation state. We should have handled those in the component’s local state and just let the app know (through actions) when something happens that should affect the whole app. For example, opening/closing a dropdown item shouldn’t trigger an action by default, but clicking a menu item is definitely something that is expected to have an effect on the app. Storing everything just generates a lot of boilerplate code, plus it gives you a lot of actions that make it hard to keep track of what’s going on.
No common thinking on using Redux with Angular together
Two things to discuss here: store selectors and action dispatchers.
Let’s start with store selectors. They can be placed in Angular components or in services. It’s easy to make a stand for either way. And if there isn’t a clear concept you communicate towards the team, the developers will start using both, depending on their personal decisions. We should’ve had a discussion about it and decide when to put the selector in components and when in services. Yes, both needed. However, it is not well-founded to put the selectors in all scenarios into the corresponding service as it generates a lot of boilerplate. As a rule-of-thumb I recommend to define selectors in components by default and use them in services when any logic needs to be applied before the view layer can take the data over.
Our other point: Action dispatchers. Here we have a similar dilemma, actions can be dispatched from components or services. I can offer you the same advice: define a set of rules and make make sure your team follows them. Personally I suggest to dispatch actions from components as long as all the information are available there for the action builder. In any other case dispatch them from a service where all necessary data is available.
Inconsecutive action names
Using actions to describe what had happened recently in the app seemed plausible to me, so:
- when the user clicks on the Login button, the action is ‘login button clicked’
- when a list of items has arrived from the backend, the action says ‘items fetched’ or ‘item list updated’
- when something goes wrong, the action is ‘error during fetching items’ or ‘password is incorrect’
BUT the rest of the team started to give commands rather than reporting events:
- ‘login button clicked’ => ‘send login request’
- ‘item list updated’ (network response has arrived) => ‘update item list’ (update the item list in the app)
- ‘password is incorrect’ => ‘show incorrect password alert’
I later realised that both ways work just fine, as long as you don’t mix them. Doing so we’ve managed to make:
- the data flow untraceable.
- the new team members confused which style to follow.
- hard to find out if the action you’d like to dispatch is defined already.
I resonate with the event-based naming, here’s why.
The more code we wrote, the more trouble we had
As you may have already realised, these issues escalated to a higher level as SLOC increased:
- It was quite challenging to debug somebody else’s code.
- It wasn’t so obvious how to extend or add functionality.
- To follow what’s happening on code level was not as easy as Flux’s single data flow direction promises.
- And this all lead to longer onboarding of new team members.
First of all, have a common understanding of the Flux architecture and the Redux library within the dev team. If needed, get your team to learn it first.
How much data you put in the store is up to you, but never forget: not all data needs to live there. For example, if it’s not required from business side to be able to track the steps taken by the user filling the form, then the form’s state can be handled as local state. Once the form is submitted, an action will be dispatched with the form data, so the application will know about it. Of course, if it’s necessary, you can turn each key press into an action and reducers can handle the form accordingly. Coincidentally, Angular-redux supports managing forms. Check it out and consider using it if it fulfills your requirements.
Have a strict naming convention for the actions as described above. It doesn’t matter what style (event or command) you choose as long as it stays consistent. Don’t be shy to leave a comment on each action’s name during code review. After a while it could feel silly to argue on wording, but it’s worth the effort to keep the code consistent.
Define actions and design the store in an early stage (it can even happen as early as the technical kickoff of the project) to prevent chaos. This document has to be accessible for every technician on the project. This will support the onboarding process, bug fixing phases and last but not least, the implementation phase as well.
Oh, and one last thing! Are you sure you need Redux for your project?
Special thanks to my colleagues Andrea Lika and Máté Safranka for helping me reviewing this article.