Sometimes the simplest implementation for a feature ends up creating more complexity than it saves, only shoving the complexity elsewhere. The eventual result is buggy architecture that nobody wants to touch.
Ngrx/store is an Angular library that helps to contain the complexity of individual features. One reason is that ngrx/store embraces functional programming, which restricts what can be done inside a function in order to achieve more sanity outside of it. In ngrx/store, reducers, selectors, and RxJS operators are pure functions.
Pure functions are easier to test, debug, reason about, parallelize, and combine. A function is pure if
- given the same input, it always returns the same output.
- it produces no side effects.
Side effects are impossible to avoid, but they are isolated in ngrx/store so that the rest of the application can be composed of pure functions.
When a user submits a form, we need to make a change on the server. The change on the server and response to the client is a side effect. This could be handled in the component:
It would be nice if we could simply dispatch an action inside the component when the user submits the form and handle the side effect elsewhere.
Ngrx/effects is middleware for handling side effects in ngrx/store. It listens for dispatched actions in an observable stream, performs side effects, and returns new actions either immediately or asynchronously. The returned actions get passed along to the reducer.
Being able to handle side effects in an RxJS-friendly way makes for cleaner code. After dispatching the initial action
SAVE_DATA from the component you create an effects class to handle the rest:
This simplifies the job of the component to only dispatching actions and subscribing to observables.
Ngrx/effects is easy to abuse
Ngrx/effects is a very powerful solution, so it is easy to abuse. Here are some common anti-patterns of ngrx/store that Ngrx/effects makes easy:
1. Duplicate/derived state
Let’s say you’re working on some kind of media playing app, and you have these properties in your state tree:
Because audio is a type of media, whenever audioPlaying is true, mediaPlaying must also be true. So here’s the question: “How do I make sure mediaPlaying is updated whenever audioPlaying is updated?”
Incorrect answer: Use an effect!
Correct answer: If the state of mediaPlaying is completely predicted by another part of the state tree, then it isn’t true state. It’s derived state. That belongs in a selector, not in the store.
Now our state can stay clean and normalized, and we’re not using ngrx/effects for something that isn’t a side effect.
2. Coupling actions and reducers
Imagine you have these properties in your state tree:
Then the user deletes an item. When the delete request returns, the action
DELETE_ITEM_SUCCESS is dispatched to update our app state. In the
items reducer the item is removed from the
items object. But if that item id was in the
favoriteItems array, the item it’s referring to will be missing. So the question is, how can I make sure the id is removed from
favoriteItems whenever the
DELETE_ITEM_SUCCESS action is dispatched?
Incorrect answer: Use an Effect!
So now we will have two actions dispatched back-to-back, and two reducers returning new states back-to-back.
DELETE_ITEM_SUCCESS can be handled by both the items reducer and the favoriteItems reducer.
The purpose of actions is to decouple what happened from how state is supposed to change. What happened was
DELETE_ITEM_SUCCESS. It is the job of the reducers to cause the appropriate state change.
Removing an id from favoriteItems is not a side effect of deleting the item. The whole process is completely synchronous and can be handled by the reducers. Ngrx/effects is not needed.
3. Fetching data for a component
Your component needs data from the store, but the data needs to be fetched from the server first. The question is, how can we get the data into the store so the component can select it?
Painful answer: Use an effect!
In the component we trigger the request by dispatching an action:
In the effects class we listen for
Now let’s say a user decides it’s taking too long for the
users route to load, so they navigate away. To be efficient and not load useless data, we want to cancel that request. When the component is destroyed, we will unsubscribe from the request by dispatching an action:
In the effects class we listen for both actions now:
Okay. Now another developer adds a component that needs the same HTTP request to be made (no assumptions should be made about other components). The component dispatches the same actions in the same places. If both components are active at the same time, the first component to initialize will trigger the HTTP request. When the second component initializes, nothing additional will happen because
needUsers will be false. Great!
Then, when the first component is destroyed, it will dispatch
CANCEL_GET_USERS. But the second component still needs that data. How can we prevent the request from being canceled? Maybe have a count of all the subscribers? I won’t bother exploring that, but you get the point. We start to hope there is a better way of managing these data dependencies.
Now let’s say another component comes into the picture, and it depends on data that cannot be fetched until after the
users data is in the store. It could be a websocket connection for a chat, or additional information about some of the users, or whatever. We don’t know if this component will be initialized before or after the other two components subscribe to
The best help I found for this particular scenario is this great post. In his example,
callApiX to have been completed already. I’ve stripped out the comments to make it look less intimidating, but feel free to read the original post to learn more:
Now add on the requirement that HTTP requests should be canceled when components are no longer interested, and it gets more complicated.
So why so much trouble for managing data dependencies when RxJS is supposed to make it really easy?
While data arriving from the server technically is a side effect, I don’t believe that ngrx/effects is the best way to manage this.
Components are I/O interfaces for the user. They show data and dispatch actions from users. When a component loads, it is not dispatching an action from a user. It wants to show data. That looks like a subscription, not the side effect of an action.
It’s very common to see apps using actions to trigger data fetches. These apps implement a custom interface to observables through side effects. And as we’ve seen, this interface can become very awkward and unwieldy. Subscribing to, unsubscribing from, and chaining the observables themselves is much more straightforward.
Less painful answer: The component will register its interest in data by subscribing to an observable of the data.
We will create observables that contain the HTTP requests we want to make. We will see how much easier it is to manage multiple subscriptions and chain requests off of each other using pure RxJS than it is to do those things with effects.
Create these observables in a service:
In the component:
Because this data dependency is now just an observable, we can subscribe and unsubscribe in the template using the
async pipe and we no longer need to dispatch actions. If the app navigates away from the last component subscribed to the data, the HTTP request is canceled or the websocket is closed.
Chains of data dependencies can be handled like this:
Here’s a side-by-side comparison of the above method vs this method:
Using plain observables requires fewer lines of code, and automatically unsubscribes from data dependencies all the way up the chain. (I omitted the
finallys originally included in order to make the comparison clearer, but even without them the requests still get canceled appropriately.)
Ngrx/effects is a great tool! But consider these questions before using it:
- Is this really a side effect?
- Is ngrx/effects really the best way to handle this?
Please share your feedback, especially any corrections you might have. If I’m not horribly wrong about anything, then my next post should be interesting, as it builds on top of this.