These days people say to me, "David, you look tired. Have you had trouble sleeping?"
The answer is yes, and here's why: There's a lot of scary code out there using NgRx Effects in ways that make it really hard to understand.
But I want to give you assurance that you can play a role in helping me sleep better tonight by making three tiny changes to your NgRx code.
First...
#1: Name effects like functions
An example will make this clearer.
Here's an effect named by what triggers it:
formValidationSucceeded$ = createEffect(() =>
// the effect body...
);
Without analyzing the body of the effect, I have no idea what side-effect this effect causes.
If we look at the internals of the effect, we discover it does something specific:
formValidationSucceeded$ = createEffect(() =>
this.actions$.pipe(
ofType(FormValidationSucceeded, ForceSaveClicked),
withLatestFrom(this.store.select(formSelector)),
switchMap(([action, form]) =>
// Oh! It saves the form.
this.saveForm(form).pipe(
map(() => SaveFormSucceeded()),
catchError(() => of(SaveFormFailed()))
)
)
)
);
Because the effect saves the form, then it should be named saveForm$
.
Naming an effect by its result really helps when an effect is triggered by multiple actions, as in the example above.
Because the effect also fires when ForceSaveClicked
, then this new effect name makes it clear that, when either action occurs, the form will be saved.
When you name effects like functions, bad effects also become more evident. Effects that do more than one thing result in names that include words like "And" or "Or".
Which leads me to...
#2: Make your effect only do one thing
Imagine if there was an effect that reacted to a user clicking a "Save" button for a form like so:
saveForm$ = createEffect(() =>
this.actions$.pipe(
ofType(SaveButtonClicked, ForceSaveClicked),
withLatestFrom(this.store.select(FormSelector)),
switchMap(([action, form]) => {
if (action.type === ForceSaveClicked.type
|| this.validationService.isValid(form)) {
return this.saveForm(form).pipe(
map(() => SaveFormSucceeded()),
catchError(() => of(SaveFormFailed()))
);
}
return of(FormValidationFailed());
})
)
);
This effect does not in fact "saveForm" every time it runs. It may not save the form if the validation fails. And even more horribly, it checks the action type because sometimes the type of action affects what code is run.
Instead, it is much clearer to split this effect into two different effects:
validateForm$ = createEffect(() =>
this.actions$.pipe(
ofType(SaveButtonClicked),
withLatestFrom(this.store.select(FormSelector)),
map(([action, form]) =>
this.validationService.isValid(form)
? FormValidationSucceeded()
: FormValidationFailed()
)
)
);
saveForm$ = createEffect(() =>
this.actions$.pipe(
ofType(FormValidationSucceeded, ForceSaveClicked),
withLatestFrom(this.store.select(FormSelector)),
switchMap(([action, form]) =>
this.saveForm(form).pipe(
map(() => SaveFormSucceeded()),
catchError(() => of(SaveFormFailed()))
)
)
)
);
Now it is sharply clear that when SaveButtonClicked
, the form is validated, and either when FormValidationSucceeded
or ForceSaveClicked
the form is saved.
(Spoiler- this makes writing unit tests even easier for the effects)
When effects only do one thing, it makes it enjoyably easy to compose understandable chains of effects that occur based on easy-to-understand actions.
Let's do another thought exercise. What if we want to validate the form every time the form changes, not just when the user clicks save, so that we can let the user know that they need to fix something? We wouldn't want the save to occur every time FormValidationSucceeded
anymore.
Assuming our validate succeeded and failed actions result in the store being updated with the form's validity, then we can compose a new effect and modify the trigger of the existing save effect:
emitFormWasValidWhenSaveFormClicked$ = createEffect(() =>
this.actions$.pipe(
ofType(SaveFormClicked),
withLatestFrom(this.store.select(isFormValidSelector)),
filter(([action, isFormValid]) => isFormValid),
map(([action]) => FormWasValidWhenSaveFormClicked())
)
);
saveForm$ = createEffect(() =>
this.actions$.pipe(
ofType(FormWasValidWhenSaveFormClicked, ForceSaveClicked),
withLatestFrom(this.store.select(formSelector)),
switchMap(([action, form]) =>
this.saveForm(form).pipe(
map(() => SaveFormSucceeded()),
catchError(() => of(SaveFormFailed()))
)
)
)
);
We didn't have to touch the validateForm$
effect at all, and we didn't have to modify the internals of saveForm$
. It's so comfortable to make changes that it almost makes me feel sleepy...
But wait! I can't doze off yet, because there's one more thing keeping me awake at night.
#3 Emit only one action
Controversy! Yes, you will see other articles around the web that show the raw power of NgRx Effects by demoing returning an array of actions from a single effect.
But if you're following rules #1 and #2, then under what scenario would an effect result in multiple actions?
The answer is... none?
For the same reason you shouldn't dispatch multiple actions in a row in other places in your application, you shouldn't dispatch multiple actions from an effect because a single effect has a singular result.
Having multiple actions dispatched from an effect is likely a code smell that your actions are commands instead of representations of the events (read- actions) that have taken place in your system.
Final Thoughts
By keeping your effects concise and free of complication, you gain readability, composability, and testability. That's where the power of NgRx Effects shines.
I'm really feeling at ease now. I hope you are, too. Goodnight ๐ค.
(Oh hey, you really made it to the end? Check out this great eslint library that enforces other good practices into your NgRx Effects: https://github.com/timdeschryver/eslint-plugin-ngrx)
Top comments (4)
These are great patterns and also make the effects way easier to test! The only thing I'm not sure about only dispatching one action per effect. Like if, on a success action, want to update state, show a notification and route to a new page, it seems like it would be duplicate code to have three separate effects that listen for the same action. And you might want those actions dispatched in a particular order.
I would argue that the notification and route navigation are commands, and should be performed thru an injected service. As to the particular order, that would be easy as you could do SuccessAction -> showNotification$ -> NotificationDisplayed -> redirectToNewPage$.
Of course, the user might miss that notification if you're immediately redirecting, so hopefully there's a delay in there (which would be easier to do with distinct effects as opposed to them all immediately emitting at once from a single effect).
I like 2 and 3 (as you already know :)).
Not so sure about #1. You say "name your effects like you name your functions" - but they're not just functions, they're event handlers. The way we name effects comes from the usual way that event handler functions are named across languages/frameworks.
We may disagree, but in my view effects are not event handlers. Effects are intended to provide a way to invoke side effects for a potential array of events and other Observable streams in the system.
If they were event handlers, then I would prescribe each effect have only one way to be triggered. And further, an event can trigger many things to occur, and therefore effects would have many results.
But then we lose the flexibility and composability of effects.
To stick with the event handler view of effects means you would need to have a duplicate effect for each event that should trigger a form save, for example. Now, you could have each event-handler-effect call some save private function, but that duplication is unneeded since we can write simpler Observable streams to react to the stream of Actions using the variatic
ofType
operator.Thinking about (and naming) effects as a true "side-effect" in a system means thinking of units of side effects that can be triggered. The beautiful thing about NgRx Effects using RxJs Observables also means something else pretty important about effects- they don't just have to be triggered by actions. Effects can be written to react to any Observable stream, which means you can write some pretty powerful and elegant triggers for very simple results.
TLDR- naming effects as event handlers leads to writing effects that are not flexible or easily composable (in this writer's opinion)