NgRx Data in Angular and Its Benefits

Valcon Serbia
Valcon Serbia
Published in
9 min readMay 8, 2023

What is NgRx Data?

Firstly, NgRx data is built on top of the NgRx store, and it uses actions and reducers internally to manage the state of the application. The key difference between NgRx data and the NgRx store is that NgRx data provides a higher-level abstraction for working with entities, so developers don’t have to manually create actions for each entity in the application.

With NgRx data, we need to define entity metadata that specifies the data structure (and optionally API endpoints) for each entity, and NgRx data then generates actions and reducers automatically based on that metadata. This can save a lot of boilerplate code and make it easier to work with complex entity relationships.

However, it’s important to note that while NgRx data takes care of a lot of the low-level details of managing the state of the application, we still need to understand how actions and reducers work in the NgRx store in order to use NgRx data effectively. This includes understanding how to create custom actions and reducers and how to handle side effects using NgRx effects.

Overall, NgRx data can be a very powerful tool for managing entity relationships in our applications. Still, it’s important to have a good understanding of the underlying principles of the NgRx store in order to use it effectively.

Main Benefits

Centralized State Management

NgRx data provides a centralized store to manage the application state. This makes it easy to share data between components and ensures that the state is consistent across the application.

Predictable Data Flow

With NgRx data, the flow in our application is strictly unidirectional, meaning that it always flows in one direction. This makes it easier to understand how the state of our application is changing over time and to predict how it will behave in different situations. By following this predictable data flow, we can avoid unexpected errors and bugs that might be caused by conflicting changes to the state of our application.

Immutable State

The NgRx data store uses an immutable state which means that the state cannot be changed directly. This helps to prevent accidental changes to the state and makes it easier to track changes over time.

An immutable state means that the state should not be changed directly but should be updated through pure functions that return a new copy of the state with the changes applied. However, it’s important to note that an immutable state does not mean that the state cannot be changed at all. In fact, it’s often necessary to update the state in response to user actions or other events in the application. In most applications, the state of the user interface needs to change based on what the user is doing or based on other events that happen within the application. For example, when a user clicks a button to create a new entry, the state of the application needs to change to reflect the new state. This is important because it ensures that the user interface is responsive and that the user has a good experience. So, updating the state of an application in response to user actions or other events is a crucial part of building a successful application.

Reactive Programming

NgRx data is based on reactive programming principles, meaning it works well with the observables and asynchronous flows. With that ability, we can handle complex data flows in a simpler and more efficient way.

Example:

// get all users and their blogs from the store as an observable
getAllUsersWithBlogs(): Observable<User[]> {
return combineLatest([this.usersEnitityService.entities$, this.blogsEnitityService.entities$]).pipe(
map(([users, blogs]) => {
return users.map(user => {
const userWithBlogs = { ...user };
userWithBlogs.blogs = blogs.filter(blog => blog.author === user.id);
return userWithBlogs;
});
})
);
}

Developer Tooling

NgRx data supports developer tools for debugging the application state. These tools include a time-travel debugger, which allows developers to go through the application state changes over time, identify issues, and improve the performance of the application.

Here are some of the developer tools that are available with Ngrx data:

a. EntityCacheDevTool
The EntityCacheDevTool is a tool provided by NgRx data that helps developers to debug the entity cache in the application.

b. NgRx DevTools
The NgRx DevTools is a browser extension that provides a time-traveling debugger for the NgRx store. It allows developers to debug the state changes in the application over time, and it is also possible to check previous states.

c. Data Persistence DevTools
The Data Persistence DevTools is a set of developer tools that includes tools for debugging the local storage and session storage and also tools for managing the persistence of the NgRx store.

d. Store DevTools
The Store DevTools is a tool that provides a real-time view of the store, including the state, actions, and effects.

What Kind of Data Should Be Stored?

The data that we store in the state store should be the data that is important for our application’s functionality and that needs to be shared across multiple components. Here are some examples of the kinds of data that are useful to keep in the store:

  1. Application settings: Settings that affect the behavior or appearance of the application, such as the language, theme, and sound configuration, can be stored in the store and shared across multiple components that need to access or modify the settings.

Example:

export class SettingsComponent implements OnInit {
settings: Settings;

constructor(private settingsEntityService: SettingsEntityService) {}
ngOnInit() {
this.settingsEntityService.getByKey('settings').subscribe(settings => {
this.settings = settings || {
language: 'en',
theme: 'light',
soundEnabled: true
};
});
}
}

2. User data: Information about the logged-in user, for example, their ID, email, or permissions, can be saved in the store and shared across multiple components that need to display or interact with user data.

3. Cached data: Data that is frequently used and that takes more time to load, such as API responses or database queries, can be cached in the store for faster access and improved performance.

getUsers(): Observable<User[]> {
const url = 'https://web-gateway/api/users';
return this.http.get<User[]>(url)
.pipe(
// Add the users to the store
tap(users => this.usersEntityService.add(users))
);
}

4. Business data: Data that is specific to our application’s business logic.

In general, the data that we store in the store should be read-only and should only be modified via NgRx data API methods.

Methods

· add(entity: T): Adds a new entity to the collection.

· addMany(entities: T[]): Adds an array of entities to the collection.

· addOneToCache(entity: T): Adds a new entity to the collection in the cache only.

· addAll(entities: T[]): Replaces the entire collection with an array of entities.

· update(entity: Update<T>): Updates an existing entity in the collection.

· updateMany(updates: Update<T>[]): Updates an array of entities in the collection.

· upsert(entity: T): Updates an existing entity or adds a new one to the collection.

· upsertMany(entities: T[]): Updates an array of existing entities or adds new ones to the collection.

· remove(id: ID): Removes an entity from the collection by ID.

· removeMany(ids: ID[]): Removes an array of entities from the collection by ID.

· removeAll(): Removes all entities from the collection.

· getLoaded(): T[]: Returns all entities that are currently in the cache.

· getEntity(id: ID): T: Returns a specific entity from the cache by ID.

· getIds(): ID[]: Returns an array of all entity IDs in the cache.

· getWithQuery(queryParams: QueryParams): T[]: Returns an array of entities that match the specified query parameters.

· getEntityAction(entity: T, entityOp: EntityOp): EntityAction<T>: Creates an entity action object for a specified entity operation (e.g. add, update, remove).

Example:

import { User } from './user.model';
import { EntityCollectionServiceBase, EntityCollectionServiceElementsFactory } from '@ngrx/data';
import { Observable } from 'rxjs';

@Injectable()
export class UserService extends EntityCollectionServiceBase<User> {
constructor(serviceElementsFactory: EntityCollectionServiceElementsFactory) {
super('User', serviceElementsFactory);
}

// get all users from the store as an observable
getAllUsers(): Observable<User[]> {
return this.entities$;
}

// get a single user by id from the store as an observable
getUserById(id: string): Observable<User> {
return this.getByKey(id);
}

// add a user to the store and return the added user as an observable
addUser(user: User): Observable<User> {
this.add(user);
return this.getByKey(user.id);
}
}

The Difference Between add and addOneToCache

The difference between the two methods is in their behavior:

· add() adds the entity to both the cache and the backend store, dispatches the needed actions, and synchronizes the cache and backend.

· addOneToCache() adds the entity to the cache only and does not persist it to the backend.

One of the actions dispatched by the add() method in NgRx data is EntityActionTypes.Add. It happens after the entity has been successfully added to both the cache and the backend store.

In both cases, NgRx data dispatches the StoreEntityAction.AddOne() action to notify all the interested parties (such as NgRx effects or reducers) that a new entity has been added to the cache or backend.

The add method in NgRx data dispatches both the EntityActionTypes.Add action and the StoreEntityAction.AddOne action, while the addOneToCache method only dispatches the StoreEntityAction.AddOne action.

The EntityActionTypes.Add action is dispatched first when using the add method, and it is used to notify all the interested parties (such as NgRx effects or reducers) that an entity is being added to the collection. StoreEntityAction.AddOne action is dispatched after the backend responds with the updated collection, and it is used by the entity adapter to update the cache.

On the other hand, the addOneToCache method only updates the local entity cache, and it does not communicate with the backend. Therefore, it only needs to dispatch the StoreEntityAction.AddOne() action to update the cache with the new entity.

Examples:

addCustomUser(addRequest: AddRequest) {
this.usersHttpService.addCustomUser(addRequest).subscribe({
next: (res: User) => {
this.customUsersEntityService.addOneToCache(res);
},
complete: () => {
// some logic
},
});
}
addCustomUser(user: User) {
this.usersEntityService.add(user).subscribe(() => {
// some logic
});

Custom Side Effects

One of the benefits of NgRx data is that it provides a lot of functionality out of the box, which can save developers a lot of time and effort. In some cases, it may be possible to build an entire application using NgRx data without a need to write custom side effects.

However, it’s important to note that side effects can be necessary for certain situations. For example, if we need to load data from a server, we may need to use a side effect to dispatch an action to fetch the data and update the store accordingly.

So, it is possible to build an entire application using NgRx data without the need to write custom side effects. However, it is important to have a good understanding of how side effects work in the NgRx store and NgRx effects, as they can be a powerful tool for handling complex data flows and interactions with external data sources.

Example:

import { Injectable } from ‘@angular/core’;
import { Actions, createEffect, ofType } from ‘@ngrx/effects’;
import { switchMap, map } from ‘rxjs/operators’;
import { User} from ‘./user.model’;
import { UserActionTypes, addUserSuccess } from ‘./user.actions’;
import { EntityActionTypes, Add } from ‘@ngrx/data’;
import { UserService } from ‘./user.service’;

@Injectable()
export class UserEffects {
addUser$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActionTypes.Add),
switchMap((action: Add<User>) => {
const user = action.payload.data;
return this.userService.addUser(user).pipe(
map((response: User) => {
return addUserSuccess({ user: response });
}));
})));
constructor(private actions$: Actions, private userService: UserService) {}
}

In this example, we defined a custom action of type UserActionTypes.Add and an effect addUser$ that listens for this action type. When the effect is triggered, it retrieves the user’s data from the action payload, then it calls the addUser() method of the UserService to add the user to the backend store and then dispatches a UserActionTypes.AddSuccess action with the newly added user as a payload.

import { Component, OnInit } from ‘@angular/core’;
import { Store } from ‘@ngrx/store’;
import { User} from ‘./user.model’;
import { addUser } from ‘./user.actions’;
import { UserState } from ‘./user.reducer’;
@Component({
selector: ‘app-user-form’,
templateUrl: ‘./user-form.component.html’,
styleUrls: [‘./user-form.component.css’]
})
export class UserFormComponent implements OnInit {
user: User = { id: null, name: ‘’, completed: false };
constructor(private store: Store<UserState>) {}
onSubmit() {
this.store.dispatch(addUser({ data: this.user }));
this.user = { id: null, name: ‘’, completed: false };
}}

Overriding API URLs

It is possible to override the default URL for a specific entity type. One of the ways is to configure the entityHttpResourceUrls property in the entity metadata.

const entityMetadata: EntityMetadataMap = {
User: {
entityName: ‘User’,
entityHttpResourceUrls: {
selectIds: ‘/web-gateway/users/users/ids’,
selectAll: ‘/web-gateway/users/ users’,
},
},
};
@NgModule({
imports: [
NgrxDataModule.forRoot({ entityMetadata }),
],
})
export class AppModule {}

We can also override the URL for specific HTTP methods.

Example:

entityHttpResourceUrls: {
selectAll: ‘/web-gateway/users/users’,
POST: ‘/web-gateway/users/user’,
},

Another way to override the URLs would be by overriding NgRx data methods in the registered UsersDataService (which should extend the DefaultDataService<User>).

override update(update: Update<User>): Observable<User> {
return this.http.put<User>(
`${environment.apiUrl}/users/user/${update.id}`,
update.changes
);
}

In this example, environment.apiUrl is our target URL (where the user’s services are exposed).

It is important to register any entity data service by adding this to the appropriate NgModule

entityDataService.registerService(User, usersDataService),

where entityDataService is of type EntityDataService and usersDataService is of type UsersDataService, as we mentioned above.

In this way, not only the URL but also the response can be modified.

Example:

override add(user: User): Observable<User> {
return this.http
.post<User>(`${environment.apiUrl}/users/user`, user)
.pipe(
map(res => {
return {
...res,
newProperty: someCustomValue`,
};
})
);
}

Conclusion

Using NgRx data in Angular projects can bring several benefits. Firstly, it simplifies the entity state management of the entire application by providing a set of predefined methods for CRUD operations. The other benefit is that it comes with a powerful set of developer tooling, making it easier to debug and optimize the application’s performance. Finally, it integrates very well with other NgRx packages, such as the NgRx store and NgRx effects, to provide a complete state management solution for more complex applications.

Autor: Jovana Vajagić

--

--

Valcon Serbia
Valcon Serbia

The home of tech enthusiasts, HRs, and fun, called Valcon Serbia. Learn about IT stuff, enjoy, and find your dream job.