NgRx Tutorial: Architecting Angular Applications with NgRx

NgRx Tutorial - Figure out how to organize and architect your NgRx application.

NgRx Tutorial: Architecting Angular Applications with NgRx

So you've finally wrapped your head around NgRx knowing how to consume actions, the reducer, effects, and setting up the state object. The question becomes, how do I effectively use NgRx within my application? Over several iterations over five different projects working with my teams, I settled on some opinions.

Goals

  1. Define the different types of state
  2. Entity State Module
  3. URL / Router State Module
  4. Container Components and State

The Three Different Types of State

Thinking of state management, the focus may shift directly to the database state as the source of truth for the entities that need to exist within the application. But that's only a third of the concerns here. The URL and Component states raise concerns in managing state and need to be addressed.

Entity State

The entity state consists of entities returned from a source of truth that users of the application share. These entities typically reside in a database and retrieved via REST calls that expose CRUD operations. External service calls within this state cause side effects, and for this reason NgRx Effects nest here.

URL / Router State

The Router State parses URL parameters and querystring values that trigger a certain state to be shown. Knowing these parameters gives the ability to filter and choose underlying data, for instance a REST call to grab an item by id. Each change of the router triggers an update for the URL parameters and querystring parameters per the router state.

Component State

The Component state includes component specific filters and properties for components to consume. Think of toggling a checkbox or typing in a textbox to filter a certain set of entities. This state stores the search text of a textbox or if that checkbox displays checked.

Free Your Developers

Nx Monorepo Starter alleviates developers from re-inventing deployments on popular Google Cloud Services, standardizes on libraries, and saves time for scaffolding projects. Invalid Email Address View on Gumroad

Thanks for your interest in Nx GCP Starter. We'll be in touch shortly

Entity State Modularization

A feature module exists per each entity for state management. I keep this separation because organizing per entity takes less mental space.

The module communicates with REST or socket services and devotes itself as the intermediary between updating states in the client and backend.

Within each module exist state, reducer, actions, effects, selectors, index (as a barrel), and entity-name.module. This organization alleviates any confusion of the expectations within each file.

Entity State

The state.ts file consists of the entity state object. In my instances, I consume NgRx's Entity Adapter and EntityState to provide niceties for a dictionary of entities based on a defined key, and an array of ids. I leave any specific UI out as other projects inherit and consume the entities state module. loading or loaded may be defined as a way to signal waiting REST calls. The loading or loaded could be more intricate for the purposes of tracking which entities remain in flux.

Entity Actions

The actions.ts file presents the specific events for the CRUD operations performed for the entities. An enumeration of ActionTypes and type of Actions consisting of the different classes to represent the ActionType and payload live within. Common actions include [EntityName] QUERY, [EntityName] CREATE, [EntityName] PATCH, [EntityName] REMOVE. Tag on SUCCESS to each of those actions in the case external state as REST calls dispatch with successful transactions. Depending on the level of granularity, a FAILURE could also be added. Most of the time, I blanket these with a [EntityName] REQUEST_FAILURE and have my NotificationActions relay the messages to the user.

Entity Reducer

The reducer file generates an immutable state object based on the actions dispatched. The entity feature adapter provides helper methods to abstract the changes for immutability, addOne, upsert, removeOne, etc. Other properties such as loaded or loading change as actions dispatch.

Entity Effects

The effects file communicates with the backend services as state changes in the underlying database that all users of the app consume. For my purposes, my effects require to communicate with Firebase as the underlying database, Firestore. Each action that requires a record change dispatches an ACTION_SUCCESS or and ACTION_FAILURE on completion with backend record changes. Firebase features the ability to stream changes providing an action type. The advantage of this allows querying once for data and watch changes making realtime app creation a breeze.

Entity Selectors

Using the entity adapter, selecting a dictionary or array of entities delivers out of the box. Then additional selectors for other pieces of the state can be divvied up and exported from the file.

Entity Module

The module lives as a feature module leaving the consuming application to import the entity module individually or through a module encompassing all entity modules.

Index

The index file barrels, aliases, and exports each of the classes and functions defined in the module, selectors, effects, actions, and reducer. Exercise caution when using this barreling technique as circular references surface when imports paths reference by the barrel rather than a relative path within the feature module.

Router State Module

The router state module filters changes per the router service events. As the route changes, query string and route parameters update in the state object.

Router State

Typically I care about three properties from the router, the URL, querystring parameters, and route parameters. The interface RouterStateUrl includes these properties. The State interface includes a routerReducer of the generic type RouterReducer exposing the generic state property, my RouterStateUrl interface, and a navigationId.

Router Reducer / Serializer

The serializer extracts the querystring parameters, route parameters, and url from the router's state. It then transforms and returns an object of type RouterStateUrl to be consumed by other selectors.

Router Selectors

The router selectors return the querystring, route parameters, and url. To ease other selectors filtering by one of the parameters on the router, a function requiring one of the route parameter names returns the value of the route parameter as defined in your route configuration. The getParamId retrieves a route parameter by name.

For example, if you routes configured at url such as api/podcast/{podcastId}, the we can retrieve the router parameter podcastId as such, getParamId('podcastId').

Consuming the router selectors within a container component is show below.

Container Component Modules

Container components orchestrate the actions dispatched from the page. When my team and I initially set these up, each page defined a container component for the route. Child components within each of the container components emitted events through their own outputs. While I bought into this architecture initially, output event emitter hell surfaced.

Child components require bubbling of other events from its children. Soon there would be three event emitters bubbling up to the container component to dispatch an action. For this reason, I recommend NgRx Everywhere.

NgRx Everywhere

NgRx Everywhere decouples the dependencies of bubbling events up through through child components. I created unnecessary events in nested components. Eventually the components required many outputs to listen in the orchestrating component. If capturing the user's action requires a state change, dispatch the necessary NgRx action.

Shared Components

Shared components display information provided or may emit data through its outputs. These components sprinkle between different modules as needed. If you've used Angular Material, the mat-button directive exemplifies this best. They are generic in nature consuming no data services, only allowing inputs and outputs.

Feature Module

The feature module composes the routes, component state module, and the components declared within.

The directory structure configured below.

Components

These components drive the events dispatched by the NgRx Everywhere philosophy. They consume data driven by selectors within the Store Module(s). They orchestrate the user driven events, such as clicking a save button to then dispatch data driven events, such as saving an entity to a database via a REST call. They direct the output event emitters from child components. Should you find that bubbling multiple events through the outputs as they nest, consider changing the events of those components to dispatch an NgRx event.

The podcast-search.component.ts reads data from the store and dispatches different events based on user input and actions.

Container Component Store

The store provides the UI state of container / smart components within the module.

Container State

The state provides different filters or user generated state via a search or selecting a textbox. In the case below, the podcastId provides an ephemeral property based on the selection of the user. Imagine there's a list of podcasts and a user selects one of the podcasts. This podcastId acts as a filter I'll use in a selector to display the chosen podcast.

Container Selectors

UI selectors borrower from different entity states slicing the different pieces of state to filter to the user's selections through UI changes.

The selectedPodcast selector combines two selectors, the UI feature state selector selectFeatureState, and a selector that provides a dictionary of PodcastModels from our entity selector PodcastSelectors.selectEntities. This allows selecting of the chosen entity driven by the UI's state.

Container Actions

The actions in the UI state module typically won't dispatch actions that cause side effects. They exist to update the filtering state properties.

Container Reducer

The reducer changes the properties of the state object via spread operators to conform to the immutability practices.

Container Effects

For many instances, effects shouldn't exist in Component Store Modules. I've had one instance where I needed an effect to search ephemeral external data to Apple's podcast search. Creating an entirely different store for this for searching and storing this data was unnecessary as it's existence lived short periods of time and changed as users keyed in podcast names.

Conclusion

NgRx architecture poses difficulties on where different types of state should exist. Keep in mind the three different types of state, Router, Entity, and Component when choosing a way to organize the code.

  1. Router State - State Exists via the URL
  2. Entity State - Entities that reside in a database acquired typically through rest calls
  3. Component State - State of the UI for filtering down entities.