NgRx. Most Angular developers should be familiar with the term. It should invoke notions of state machines and modern architecture. NgRx has become largely fundamental to Angular development today, much like Redux became fundamental to React development. Here at Briebug, we LOVE NgRx, as a concept and pattern, as well for its high-level architecture.
Like recent opinions about Redux, NgRx may also evoke concurrent feelings of both love and hate, as there are indeed things to adore…and sometimes things to loath about the framework as it has been implemented thus far today. The terms “boilerplate” and “heavy code” and “tedium” may come to mind, as sadly these are some of the natural side-effects of NgRx due to its (purposeful) design.
So, what is NgRx Auto-Entity? Derived from “generic actions”, the brainchild of Briebug CEO Jesse Sanders, and the pet project of several of us Briebuggers for a number of months in our rare spare moments of free time. NgRx Auto-Entity is an add-on library that aims to address the heavier nature of NgRx, to eliminate the necessity of what has colloquially become termed “boilerplate” from what is otherwise a wonderful concept and solid design principal. In a nutshell: Defining Actions! (And handling them…)
NgRx Auto-Entity provides a set of ready-made generic entity actions, supporting standard CRUD operations as well as covering a few common NgRx usage patterns, along with a set of pre-made and fully encapsulated effects and reducers, that can be used to automate standard entity CRUD with NgRx.
NgRx Auto-Entity is fundamentally nothing different from the normal NgRx you are already familiar with, and we aim to support continued use of standard NgRx best practices… in the occasions they are absolutely necessary. Our goal is, ultimately, to make the great parts of NgRx easy and pleasant to use by offering ready-made solutions for the most frequent use cases (and the largest source of “boilerplate”), while still allowing complete and total flexibility and autonomy by the developer when necessary.
This article aims to be a complete and thorough introduction. As such, it may be a little longer than your normal 5 minute read. I’ve attempted to break the article into logical sections, which may also serve as useful stopping points for those who may not wish to sit and read the whole thing at once.
The Problem
When discussing NgRx with fellow colleagues and other Angular developers in the community, as well as when reading about NgRx in other articles around the Internet, the most common complaint is the sheer volume and dispersion of code necessary to define and create usable actions, create entity state, implement common reducer patterns, implement effects, etc.
NgRx requires a lot of repetition, especially around actions and all the bits and pieces and little parts that make an action a fully usable part of NgRx. While technically each individual action and each individual reducer case may be “unique” in the sense that they handle different distinct actions, we generally tend to write the same kind of code, the same exact patterns and structure of code, repeatedly with NgRx.
Other Solutions
There are existing solutions to this necessity of repetitive code. Many of you may be familiar with Angular Schematics, which are a powerful and extensible CLI framework for generating source code and adding it to Angular projects. Code generators can take away a lot of the tedium involved in implementing NgRx.
All of the necessary code for each action, each entity reducer, and some of the code for effects can be created in about 30 seconds with a custom Angular schematic. In fact, Briebug has provided our own library of schematics that do just this, and they are available here! Briebug’s ngrx-entity-schematics are designed to fill exactly this niche.
Another solution that has been around for a while are “snippets”, an add-on for VSCode (and perhaps other IDEs) that allows you to type in a simple short code that expands instantly into all of the necessary code for a new action, or a new reducer or effect. Snippets are great for adding new individual actions or for quickly boilerplating a non-entity reducer…but they have their limitations.
These two solutions address some of the tedium issues with NgRx. They take care of most of the need to manually write a lot of the code. That said…both solutions still leave you with a rather large volume of NgRx code in your codebase. That code is also often still dispersed rather widely through your codebase. There are different schools of thought on just how necessary actually spelling out each and every action really is, and the exact value here may depend on the project and its scale.
For anyone who has worked on larger projects, or even for anyone who is itching for simpler, smaller, cleaner codebases, you may still find that this is not an ideal solution. State related code can add a fair amount to the overall size of your minified codebase and it adds to the size and complexity of your folder and file structure when writing code. Some larger projects may still end up with thousands of actions, nearly as many effects and hundreds of reducers.
What about angular-ngrx-data?
Another solution that is currently available is angular-ngrx-data (soon to be @ngrx/data). This library, created by John Papa and Ward Bell, is another attempt to greatly simplify NgRx usage and reduce the overall complexity of using a reactive state machine in your applications.
For some projects, angular-ngrx-data may be the most appropriate option. If your ultimate goal is to reduce the necessary code footprint to nearly zero, then this may be the library for you. This library takes more of a configuration-centric approach, allowing you to configure various aspects of the library and your entities. It is worth taking a look to see if it fits your needs!
Our NgRx Auto-Entity library takes a different approach and tries to solve the same problem in a different way, one that is not “zero-effort”, is still very low-effort, yet also highly flexible, addressing a wider range of needs and still familiar and standardized. Rather than a configuration-centric approach, we take a functional and programmatic approach, and one that works with existing patterns and practices in the underlying NgRx and Angular frameworks rather than orthogonal to them.
The Auto-Entity Solution
Now enter NgRx Auto-Entity. The fundamental goal is to provide a set of ready-made, reusable common actions that may be dispatched for ANY entity, without the need to create a new set of action/effect/reducer (“boilerplate”) code (either manually or generated) every time you need to include different entity in state.
When you wrap up your Product implementation and are ready to move onto Inventory, then Orders and LineItems, then Customers, etc. you can continue to reuse a single set of generic actions.
NgRx Auto-Entity’s other goal is to minimize the amount of code required to initialize a new piece of state. You will no longer need to create any actions. You will not need to implement a reducer, nor will you need to manually configure an Entity Adapter. You will also not need to implement any effects.
The sole exceptions here are if you have a custom and unique need not addressed by our standard CRUD actions or companion actions.
To that end, if a custom action is required, for any reason, NgRx Auto-Entity is an add-on library. It in no way limits your ability to use NgRx the way you always have. If you have a custom need, and require the ability to implement your own actions, your own effects and your own reducers, you may do so at will. We aim not to be a barrier to accessing and leveraging the full low-level power that NgRx offers.
When NgRx Auto-Entity is present, we may still be able to help you reduce the amount of code you must implement, as many initiating actions may in turn dispatch Auto-Entity Success or Failure generic actions in order to integrate data retrieved with your own custom actions with Auto-Entity managed state. Flexibility is a fundamental tenant of NgRx Auto-Entity.
Auto-Entity Architecture
NgRx Auto-Entity (NgRx-AE for short) is an add-on library to NgRx. It is not an alternative to NgRx, it in fact is built on top of NgRx utilizing standard NgRx implementation patterns.
NgRx Auto-Entity aims to provide a more Angular-like (i.e. decorated) approach to entity state, a simpler, unified approach that requires minimal manual implementation on the developer’s part. General usage of NgRx Auto-Entity can generally follow the normal, familiar patterns you already use with NgRx.
Additionally, NgRx Auto-Entity also offers a more fully encapsulated usage pattern, facades, that we hope will simplify your usage of a reactive state machine even more!
Our Actions
At its core, NgRx Auto-Entity provides a set of ready-made standard CRUD actions, which cover the most common and prolific use cases for NgRx in general. We support a variety of means of Loading (GET) data, including individual entities and lists of entities, as well as loading pages or ranges of entities.
We also support your standard Create (PUT), Update(PATCH),Replace (PUT), and Delete (DELETE). Further, we provide a set of actions for other common NgRx entity-related behaviors, such as Select, Deselect and Clear.
Utilizing our actions requires minimal change from what you are already familiar with when it comes to NgRx actions. Our actions are newed up the same way, with the additional requirement that the model type be passed in as the first argument:
this.store.dispatch(new LoadAll(Customer)); this.store.dispatch(new Create(Customer, newCustomer)); this.store.dispatch(new Replace(Customer, updatedCustomer)); this.store.dispatch(new Delete(Customer, customer));
In order to support all of the potential requirements when requesting entities, each generic “initiating” action also supports custom criteria as the last argument. This custom criteria is typed as any, thus allowing you the developer to pass whatever you so desire here. This custom criteria is then made available in your entity services to support any form of entity request you may require, to any kind of backend service, regardless of how custom or complex, or even if it is hosted in a standardized cloud service like FireBase.
Effects and Reducers
Auto-Entity ships with a set of ready-made, fully functional effects and reducers for entity state. You are no longer required to implement any effects at all, unless you find a need for a custom action. You must initially register our effects class with the NgRx Effects module, same as with any other effects class.
You are also not required to implement any reducers, again unless you find a need for a custom action. Auto-Entity handles all of it’s own entity reduction through a single, global meta-reducer. This meta reducer must be added to your list of meta reducers for your state, once that is done any Auto-Entity actions that are dispatched are reduced automatically.
A basic, “empty” reducer (stub reducer) is required for each entity state simply to support NgRx’s standard state and reducer map configuration and allow the reducer map to be used in the @NgModule decorator, however, such reducers can be extremely minimal and bare-bones:
export function customerReducer(state = initialState): IEntityState<Customer> { return state; }
You may note from the above, we have also eliminated the need for you to create a state interface. Any Auto-Entity state may leverage our IEntityState<TModel> interface which encapsulates all of the necessary data to fully support Auto-Entity’s internal meta-reducer.
Finally, you are also no longer required to manually configure initialState. Auto-Entity provides a basic utility function buildState, that handles the creation of initial state as well as standard selectors for you:
const { initialState, selectors } = buildState(Customer); export const { selectAll: selectAllCustomers, } = selectors;
Auto-Entity provides all of the standard selectors that are also provided by a normal NgRx/Entity adapter, as well as a number of additional selectors that facilitate other built-in features of Auto-Entity.
In fact, our buildState utility function will provide you with a lot of ready-made facilities, including the initialState, all necessary selectors to retrieve the state Auto-Entity manages, an initial starter reducer (same as the example above, all you need to do is export it with a unique name), as well as a getter function entityState to retrieve the entity state object that can be used in creating custom selectors.
You may also include additional initial state as the second parameter to the buildState utility function, which allows you to prepare additional initial state for custom effects and/or reduction:
const { initialState, selectors, reducer: customerReducer, entityState: getCustomerState } = buildState(Customer, { myCustomState: … });
For more information about how to set up a project to use NgRx Auto-Entity, read our Quick Start documentation on GitBook. This will walk you through the few steps required to prepare your project to use this library and start automating your entity state.
The Developer’s Responsibility
Once you have set up your project to use NgRx Auto-Entity, there are two pieces of code that are still the full responsibility of the developer. These two aspects of any state are usually unique on a per-implementation basis, and cannot easily be “automated”. These include the entity model and the entity service. The developer is usually responsible for these two pieces of code regardless, however we hope that Auto-Entity may assist you here as well.
The Model
With Auto-Entity we must enforce one small change to your classic entity models. Rather than implementing them as interfaces they must be implemented as classes instead. This is necessary, as NgRx Auto-Entity requires runtime type information in order to work its magic.
An interface is a compile-time only construct within the context of TypeScript, while a class is, pardon the pun, a “first class citizen” of native ECMAScript.
Defining a model is as simple as ever, with one additional requirement above and beyond replacing interface with class. Auto-Entity aims to make entity state more Angular-like by using decorators where they seem appropriate. One such case is specifying the key of your entity types:
import { Key } from '@briebug/ngrx-auto-entity'; export class Customer { @Key id: number; name: string; catchPhrase: string; }
Note the @Key decorator here. This simple decorator marks the id property of the Customer model here as the entity key. NgRx Auto-Entity will then utilize this metadata within our standard side effects and meta-reducers to support proper reduction of entities. In the event that an entity has a composite key, Auto-Entity supports that as well…simply decorate all properties of the model with @Key and the library takes care of the rest.
NOTE: Auto-Entity provides a number of utility functions to support your development of applications with the library. This includes a getKeyFromModel function that may be used to retrieve the entity key of any entity from its model type.
The Entity Service
The next piece of code that is still the responsibility of the developer is the entity service. These are classically standard Angular services, however, with Auto-Entity we add the additional requirement that our IAutoEntityService<TModel> interface be implemented. This interface ensures that your entity services provide the necessary functionality for NgRx Auto-Entity to properly interact with them. Further, we also require that entity services be provided within Angular’s providers in a very explicit manner:
import { NgrxAutoEntityModule } from '@briebug/ngrx-auto-entity' import { Customer } from 'models'; import { CustomerService } from 'services/customer.service'; @NgModule({ declarations: [AppComponent], imports: [BrowserModule, NgrxAutoEntityModule], providers: [ { provide: Customer, useClass: CustomerService } ], bootstrap: [AppComponent] }) export class AppModule {}
Auto-Entity leverages a useful aspect of Angular’s built-in injector and provider model: The ability to map a custom implementation for a given provider type.
In our case, we require that the entity Model type be specified as the provider, and mapped to a useClass style implementation provided by your entity service. This is necessary, as at runtime the only piece of information NgRx Auto-Entity has initially to dynamically find the appropriate service to invoke to handle entity requests for a given model is the model itself.
Implementing an entity service is fairly straight forward. We specify a range of possible functions that may be implemented on our IAutoEntityService interface, however, all of them are marked as optional. This allows you to implement what is necessary to support the various actions to require for each entity, without requiring that you boilerplate unnecessary code.
The IAutoEntityService interface is then more of a guide to implementation than enforcing a specific implementation.
import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { IAutoEntityService, IEntityInfo } from '@briebug/ngrx-auto-entity'; import { Customer } from 'models/customer.model'; import { environment } from 'environments/environment'; @Injectable() // Must be provided via custom Model -> Service mapping! export class CustomerService implements IAutoEntityService<Customer> { constructor(private http: HttpClient) { } load(entityInfo: IEntityInfo, id: number): Observable<Customer> { return this.http.get<Customer>( `${environment.rootUrl}/customers/${id}` ); } loadAll(entityInfo: IEntityInfo): Observable<Customer[]> { return this.http.get<Customer[]>( `${environment.rootUrl}/customers` ); } create(entityInfo: IEntityInfo, entity: Customer): Observable<Customer> { return this.http.post<Customer>( `${environment.rootUrl}/customers`, entity ); } update(entityInfo: IEntityInfo, entity: Customer): Observable<Customer> { return this.http.patch<Customer>( `${environment.rootUrl}/customers/${entity.id}`, entity ); } delete(entityInfo: IEntityInfo, entity: Customer): Observable<Customer> { return this.http.delete<Customer>( `${environment.rootUrl}/customers/${entity.id}` ).pipe(map(() => entity)); } }
There are many additional methods that may be implemented. Each method provides a standard set of parameters, including the IEntityInfo metadata that describes your entity, the entity itself or its keys, as well as custom criteria that may be passed with any Auto-Entity action. Custom criteria supports complex entity requests, such as those that may require knowledge of parent keys, related keys, etc.
Shared Entity Services
One of our primary tenants with NgRx Auto-Entity is to support the smallest codebase possible whenever possible. While implementing an entity service is still the responsibility of the developer, we attempt to support simple architectures as well as complex ones. If you have a very basic entity API that follows explicit, common patterns for all entities, a single entity service may be used to handle all entity requests to your API:
@Injectable() export class EntityService implements IAutoEntityService<any> { constructor(private http: HttpClient) {} load(entityInfo: IEntityInfo, id: any): Observable<any[]> { const url = `${environment.baseUrl}/${entityInfo.modelName.toLowerCase()}s/${id}`; return this.http.get<any[]>(url); } loadAll(entityInfo: IEntityInfo): Observable<any[]> { const url = `${environment.baseUrl}/${entityInfo.modelName.toLowerCase()}s`; return this.http.get<any[]>(url); } create(entityInfo: IEntityInfo, entity: any): Observable<any[]> { const url = `${environment.baseUrl}/${entityInfo.modelName.toLowerCase()}s`; return this.http.post<any[]>(url, entity); } update(entityInfo: IEntityInfo, entity: any): Observable<any[]> { const url = `${environment.baseUrl}/${entityInfo.modelName.toLowerCase()}s/${entity._id}`; return this.http.patch<any[]>(url, entity); } replace(entityInfo: IEntityInfo, entity: any): Observable<any[]> { const url = `${environment.baseUrl}/${entityInfo.modelName.toLowerCase()}s/${entity._id}`; return this.http.put<any[]>(url, entity); } delete(entityInfo: IEntityInfo, entity: any): Observable<any[]> { const url = `${environment.baseUrl}/${entityInfo.modelName.toLowerCase()}s/${entity._id}`; return this.http.delete<any[]>(url); } }
This single entity service may then be mapped to multiple entity model providers as necessary:
providers: [ { provide: Customer, useClass: EntityService }, { provide: Order, useClass: EntityService }, { provide: LineItem, useClass: EntityService }, // … ],
Entity Façades
True to tradition, we save the best for last! Façades! You may have encountered this term while trawling the Internet for NgRx articles. This is a pattern we at Briebug have been embracing with open arms, and we wish to bring the wonder and simplicity of state façades to the masses with NgRx Auto-Entity.
A façade by definition is:
An object that provides a simplified interface to a larger body of code, such as a class library.
In combination with NgRx, a façade presents a simpler interface into the complexity of using the store, dispatching actions, and observing selectors. Façades are a great way to pull out a lot of the additional code and complexity involved with interacting with NgRx from your components. Façades encapsulate and isolate store interactions, allowing them to be more easily used, and shared, by one…or many…components in your UI.
NgRx Auto-Entity, with it’s latest beta version v0.1.0, now integrates support for prefabricated façade base classes. A facade base class is generated when you call buildState. You may destructure this class from the tuple returned, and rename it to a proper class name at the same time:
export const { initialState, facade: CustomerFacadeBase } = buildState(Customer); export function customerReducer(state = initialState): IEntityState<Customer> { return state; }
Once you have exported the façade with a unique type name, you may then implement your own façade class by extending it:
import { CustomerFacadeBase } from '@state/customer.state'; import { Customer } from '@models'; @Injectable({ providedIn: 'root' }) export class CustomerFacade extends CustomerFacadeBase { constructor(store: Store<AppState>) { super(Customer, store); // Invoke the CustomerFacadeBase constructor with your model type and the store } // TODO: Add your own custom facade API here!! }
This approach, of generating a base façade class that then must be extended, is required due to some of the limitations in how Angular works. Notably…we need to inject the store, but also need to specify the model type. Technically speaking, it should be possible to create a custom provider using a factory for the façade type returned by buildState, however doing so is a little odd, and can get a little messy:
import { CustomerFacade } from '@state/customer.state'; import { Customer } from '@models'; export function customerFacadeFactory(store: Store<any>) { return new CustomerFacade(Customer, store); } @NgModule({ providers: [ { provider: CustomerFacade, useFactory: customerFacadeFactory, deps: [Store] ] }) export class StateModule { }
Now imagine creating a new factory for each and every façade type, and registering each one, or perhaps creating a more complex factory to support multiple façade classes by model type. While possible, we prefer the child class/extension approach, as this offers the developer the option to add their own code to the façade, thus extending the API and encapsulating more reusable functionality.
Entity Façades in Practice
With façades, using Auto-Entity in your components becomes even easier than ever! You no longer need to worry about the store, selectors, dispatching, or any of the things you normally have to deal with when it comes to bringing app state into your components.
@Component({ selector: 'app-customer', templateUrl: './customer.component.html', styleUrls: ['./customer.component.scss'] }) export class CustomerComponent implements OnInit { customer: Observable<Customer>; valid = false; private updatedCustomer: Customer; constructor(private activatedRoute: ActivatedRoute, private customerFacade: CustomerFacade) {} ngOnInit() { this.customer = this.activatedRoute.paramMap.pipe( filter(params => params.has('id')), map(params => +params.get('id')), tap(id => { this.customerFacade.selectByKey(id); this.hasCustomerWithIdInState(id) .pipe(first()) .subscribe(exists => { if (!exists) { this.customerFacade.load(id); } }); }), switchMap(() => this.customerFacade.current) ); } hasCustomerWithIdInState(id: number): Observable<boolean> { return this.customerFacade.ids.pipe( map((ids: number[]) => ids.indexOf(id) > -1) ); } onCustomerChange(payload: { customer: Customer; valid: boolean }) { this.valid = payload.valid; if (this.valid) { this.updatedCustomer = payload.customer; } } onSave() { if (!this.valid) return; if (this.updatedCustomer.id == null) { this.customerFacade.create(this.updatedCustomer); } else { this.customerFacade.update(this.updatedCustomer); } } }
Clean. Simple. Elegant! Fully encapsulated state management, without the complexities of selecting and dispatching. The code describes itself without effort. Façades provide a higher level, logical & simplified API in front of NgRx state management. They are also very easy to extend. Let’s say we wanted to further simplify the logic in the onSave method above, so we can simply call customerFacade.save(customer). All we need to do is add a new method to our CustomerFacade class:
export class CustomerFacade extends CustomerFacadeBase { constructor(store: Store<AppState>) { super(Customer, store); // Invoke the CustomerFacadeBase constructor with your model type and the store } save(customer: Customer): void { if (!customer) return; if (customer.id == null) { // NOTE: == checks for both null and undefined! this.create(customer); } else { this.update(customer); } } }
Our onSave method can now be reduced to this simple beauty:
onSave() { if (!this.valid) return; this.customerFacade.save(this.updatedCustomer); }
We here at Briebug encourage you to explore the power, simplicity and cohesive benefits of using façades in your Angular/NgRx applications. We also encourage you to leverage the prefabricated façade base classes generated by NgRx Auto-Entity to help jump start your exploration of these wonderful types and all the benefits they can offer.
Extra Goodies
Our library doesn’t stop at just providing a core set of CRUD actions. In fact, we aim to automate all of the more common NgRx patterns for entity related state. This includes supporting things like selection of individual entities, clearing of entity state, as well as support for loading discrete pages or continuous ranges of entities. We also provide a wide range of standard selectors that provide temporal feedback about the ongoing state of a given action.
As a quick example, whenever a Load, Create/Update/Replace or Deleterelated action is issued, we maintain isLoading/isSaving/isDeleted state as well as loadedAt/savedAt/deletedAt timestamps. We also provide a range of ready-made selectors so that you, the developer, may observe this state at any time.
Auto-Entity includes intrinsic support for the loading and proper state management of both pages or continuous ranges of entities, as well as loading data to replace the entities already in state, or augment (merge into) entities already in state.
Loading a page of entities will, by design, always replace in state any previous set of entities for the given model with the entities of the loaded page. This is intended to support literal paged display of large (or potentially massive) lists of entities from a large database.
In contrast, loading a range of entities will simply append in state any additional ranges of entities to those already loaded into state. This is intended to support things like “infinite scrolling”, a popular pattern for dealing with large continuous data sets these days on both phones and desktop browsers.
Another feature of Auto-Entity is the ability to selectively choose which effects are actually applied. As part of our goals of providing the end developer as much flexibility as possible, we provide a number of different classes containing different sets of effects, giving you the developer control.
You may include all effects for all types of generic actions, or you may choose to include only the effects for loads and manually implement CUD actions yourself. You may selectively include any set of actions for any subset of generic actions if you so desire.
Future Goals
NgRx Auto-Entity is an evolving library, as well as an evolving concept. Many common usage patterns exist within the scope of utilizing NgRx to store entity state.
A long-term goal we here at Briebug have for this library is to expand it to support more of these common usage patterns. This may occur as additions to the core NgRx Auto-Entity library, or potentially as companion libraries that may be selectively added to your project to support one
or more new features.
Ideas that we are currently tossing around include decorator-guided automatic Loading, Saving, and Deletingtoasts or snackbars. Complete with proper and automatic integration with the UI component library of your choice.
Other potentials include prefabbed shared entity service implementations for flat APIs as well as hierarchical APIs. Improved control over just how the automatic effects handle the generic actions you dispatch (notably, which mapping operator is used, as we believe there is a time and a place for all of them: switchMap, exhaustMap, concatMap, flatMap/mergeMap).
Join Us!
We invite you to give NgRx Auto-Entity a try today! While this is currently considered a beta library, a lot of effort has gone into designing a cohesive architecture and API. The library has been tested with several actual real-world projects here at Briebug for the last several months and has been fairly thoroughly debugged. That said, it is still an early-stage library, and if you encounter any issues please let us know on our GitHub repository.
You may install NGRX-Auto-Entity at the command line:
npm install @briebug/ngrx-auto-entity yarn add @briebug/ngrx-auto-entity
Thanks for reading! We look forward to simplifying your life as an NgRx developer!