NgRx — Cherry Picking the Meta
AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!
This post is an amalgamation of techniques shared by the NgRx
community, with just a step beyond the introductory nature of most of the great posts out there. Here are some of the things it will cover:
- Action dispatches based upon navigation
- Fetching data and handling errors
- Gating Action dispatches (Skip unnecessary API calls)
- Call State Reducers and Triggers (Handling Loading and Erroring States)
- Entity Detection (This thing loaded?)
Here are the various libraries that are utilized:
@nrwl/nx
— DataPersistence.navigation, fetch@ngrx
— store, router-store, effects, entity
Usage of the libraries and helper utilities will be showcased first. This is so we can get a feel for the developer ergonomics that the techniques help accomplish, and then we’ll follow up with some implementation details.
Deep details of setup and what each library does will be skipped, and there is a list of learning resources and docs in the conclusion.
The Base Reducer
What can we extrapolate from looking at that ☝️reducer
?
- There is a probably a top level state key called
movies
- There are CRUD helpers and a normalized storage mechanism available via @ngrx/entity for
movies
- When movies are loaded, they’re set in the state tree
- We start with some
initialCallState
, whatever that is…
Our application is handling
loading
anderroring
states formovies
, it’s just not obvious yet.
When we need to handle CRUD scenarios for a movie
in the state tree we interact with that base reducer as we normally would, and we let a callStateReducer
handle loading
and erroring
states.
Here👇 is what the state tree would be initially:
When we navigate to the movies
route an action will be dispatched and an API call will be made. The call state will reflect that an API call is in progress for a batch 👇 of movies
:
After the movies are loaded the call state will again signify that it is no longer loading
, but it’s 👇 back at a resting
state:
The Call State Reducer
☝️That is the reducer that gets registered in a feature data access module 👇
The Call State Triggers
The baseReducer
was passed to the callStateReducer
along with these moviesCallStateTriggers
. They are responsible for declaring when the call state gets triggered for a change.
- For actions that kick off API calls, add the corresponding action to the appropriate
loading
collection, add as many as you need - If the API call was successful and data is loaded into the store, add those corresponding actions to the
resting
collection - Follow the same pattern for errors
Before looking at implementation details for call state we’ll switch to another piece of the puzzle, kicking off Actions
and data fetching.
Dispatch Actions Upon Navigation
What can we extrapolate from looking at that ☝️effect
?
- The DataPersistence library from Nrwl Extensions is being used
- The
MoviesFeatureShellComponent
is setup in the Angular Router, because theDataPersistence
navigation
helper looks for the component to be routed to, and then executes therun
callback - Work we want to associate with navigation has been separated from the component tree
Opinion: While
ngOnInit
is low hanging fruit for kicking off something to occur upon navigation, it introduces unnecessary coupling for our intentions and it’s limiting in comparison to what we get from moving the work into aneffect
.
- We have access to the
ActivatedRouteSnapshot
in therun
callback, as well as areadonly
snapshot of the state tree - We only dispatch an
Action
to request all movies if none are loaded, mitigating redundant API calls
Note: This gating technique is meant as an example — be sure to adjust your gating logic to match your applications needs.
Here’s what a detail navigation trigger could look like 👇
Fetch Data and Catch Errors
A few notes here:
- These data fetching
effects
are being triggered by the output of our navigationeffects
. Since the data fetching and navigation concerns are separated, we can update what happens for navigation without having to update what happens for data fetching 👏 - The first parameter in the
run
callback forfetch
is anAction
, not theActivatedRouteSnapshot
like in thenavigation
helper - If an error occurs in
run
, theonError
callback is called and the stream won’t blow up 🚫💥🤘
Quick Midway Recap
What have we covered so far?
- Saved
movies
into the state tree - Separated associated navigation logic from the components (get outta here with that!)
- Triggered a fetch of
movies
based upon navigation - Skipped unnecessary
Action
dispatches and avoided redundant API calls - Fetched a batch of
movies
in a list/summary view - Utilized the
ActivatedRouteSnapshot
to fetch a singlemovie
- Reflected the loading state of an API call in the state tree
- Caught errors from API calls and reflected that in the state tree
Which pieces are we missing?
- Selecting information from the store to use in our component trees (are we loading? loaded? was there an error? where’s my data?)
- Implementation details (moviesDetector, callStateReducer)
We’re also missing connection directly to the view and we’re not covering that in this post. See the resources list in the conclusion for posts that will cover that.
Exposing MoviesState Streams via a Facade
What can we extrapolate from looking at that ☝️facade
?
- All the information that we care about for
movies
is exposed as differentObservable
streams. Thefacade
can be injected into the component tree and you can pull out what you need - We’re able to get the
movieByRoute$
which means that router information is available at theselector
level - We can communicate
loading
anderroring
states to our component trees effectively for single or batch fetches of movies
Opinion: Facades are helpful. They can decouple your application from specific state management implementation lock-in, provide an easy-to-reason about API for data in your application, and potentially simplify testing. They’re also a helpful tool for refactoring if you are upgrading an existing app to use
NgRx
for the first time — do Facades first, then hook them up to a store. They are not required, and won’t always provide enough benefit to justify using the pattern.
Enabling the Facade with Selectors
What can we extrapolate from looking at these ☝️selectors
?
- There is a
getCallStateSelectors
factory function giving us access to all thecallState
selectors we care about - We have router information in the store so it can be used at the
selector
level, which helps us with thegetMovieByRoute
selector. This enables us to treat the router as a source of truth - We see our
moviesDetector
again that is helping us determine if a set ofmovies
is loaded — note that it’s being used here at theselector
level, and also some saw some usage in our navigationeffects
Diving into Implementation Details
Let’s look first at the moviesDetector
:
When I say entity detection, I’m referring to the act of checking for the existence of either a single specific entity or a batch, denoted by these types of checks:
- allLoaded — batch
- noneLoaded — batch
- oneLoaded — single
- notLoaded — single
You can see that the moviesDetector
is still just wrapping around another helper, the entityDetectorMap
. You could use the entityDetectorMap
, or just the different entity detectors directly, and the purpose of the moviesDetector
is to help with type safety and readability.
The Entity Detector — Implementation
Some of the goals with these entity detector helpers:
- Type safety
- Ergonomic — These should be easy to use, read, and reason about
- Reusability
Here’s an example for a pageDetector
, checking to see if some page information has been loaded:
The Call State Reducer — Implementation
👆That’s the shape and typings for callState
, which we extend for slices of state where we want to use the callStateReducer
👇
Here👇 are all of the callStateReducer
implementation details:
What can we extrapolate from looking at the that ☝️implementation?
State
andActions
are being passed into thecallStateReducer
just as any other reducer- It updates state, but only updates state when the call state triggers we set up are present, and only touches the
callState
- This reducer is reusable for any number of state slices, lifting the micromanagement of the
loading
anderroring
states out of the base reducers - The last step is that the base reducer is called, just like
NgRx
is expecting - The
getCallStateSelectors
factory function takes a feature selector and will return selectors for all the different pieces ofcallState
that we would care about - An
error
would clear out automatically upon the next fetch / successful load of data
Thoughts on Single and Batch
I don’t often see a distinction made with loading
and erroring
states between a single
or a batch
set of entities being fetched. I make a distinction because it lets me dial in to create very distinct experiences, and I don’t “cross the wires.” When a single entity is being fetched, and the application also happens to fetch a batch of entities, I want those distinct fetching scenarios to be reflected accurately. A single entity being loaded should not signify that a batch of entities finished loading. This helps keep the atomic nature of Event Sourcing intact.
Thoughts on Gating Techniques
Every application can differ in loading expectations, so be sure to gate action dispatches in a way that matches your applications needs.
Thoughts on the DataPersistence Library
The DataPersistence
library is full of helpful stuff, but it’s not required. You can work out the same patterns without it, you’ll just have to implement similar logic that the library is handling for you. It has some nice goodies like Optimistic
and Pessimistic
update helpers that I did not show off.
Conclusion
The techniques in this post are what I’ve cherry picked and adapted from the different solutions out there provided by the NgRx
community. If they work for you, great! Be sure to adapt them to your needs.
Example: You might need to gate an API call for an entity based on a summary/detail already being loaded, for which you’d need to adjust the detection strategy that was showcased in this post.
It’s important to understand that these challenges (loading data, gating API calls, dealing with errors, selecting relevant information, etc) exist regardless of the state management solution or JavaScript framework/ecosystem we’re using. Understanding these challenges and working through solving them can be applied to any number of different projects and tooling, so it’s helpful to become familiar with the problem domain.
Deriving loading
, loaded
, and error
states outside of storing information in the state tree is entirely possible and some people think that it’s the right way to do it. Don’t store what you can derive, and make derivations convenient. Do what’s right for your team. I suggest sticking to idiomatic and community driven solutions, they’re good for other people to pick up on and for you to come back to later.
I intentionally did not go into a lot of error
handling other than setting any
in callState
because what we do with error information is highly dependent on the design of the error handling journey.
Check Out Pentacle
I put some time into Pentacle this year, which is an aggregate of idiomatic, battle-tested solutions with learning resources about reactive systems and tools like RxJS, Angular, NgRx and more.
Check out the intro post or peek at the currently available resources.
Read The Docs
- NgRx — Getting Started
- NgRx — Effects
- NgRx — Router Store
- NgRx — Entity
- Nx — DataPersistence
- Nx — What is it? (hint: it’s more than the DataPersistence library)
Review These Posts
- Posts tagged NgRx on Angular In Depth (AiD)
- AiD — How to Start Flying with Angular and NgRx
- AiD — Handling Error States with NgRx
- AiD — NgRx: How and where to handle loading and error states of AJAX calls?
- AiD — Handle API call state NICELY
- AiD — NgRx: Parameterized selectors
- NgRx + Facades: Better State Management