Manage entities using @ngrx/entity
Author: @Phhansen
Manage entities using @ngrx/entity
When working with collections or arrays in our state, we tend to write many repeated code when we want to add, update or delete items from these collections.
In NgRx, they have created a helper library called @ngrx/entity adapter. The adapter provides a simple API to manipulate and query these collections, hiding much repetitive code needed.
Let's say we have a collection of type Item
as part of our component state;
interface Item {
id: string;
name: string;
}
interface ComponentState {
items: Item[];
loading: boolean;
}
Now, if we want to add one item to our array (in an immutable way), we replace the items
array in the state with a new reference.
- Class Based (Classic)
- Functional Creation (NEW)
import { RxState } from '@rx-angular/state';
import { Component } from '@angular/core';
@Component({
selector: 'my-component',
})
export class MyComponent {
addItem$ = new Subject();
constructor() {
super();
this.connect(this.addItem$, (oldState, itemName) => {
const newItem = {
id: uuid(), // unique hash generation fn()
name: itemName,
};
return {
...oldState,
items: [...oldState.items, newItem],
};
});
}
}
@Component({
selector: 'my-component',
})
export class MyComponent {
readonly #state = rxState<ComponentState>();
readonly addItem$ = new Subject<string>();
constructor() {
this.#state.connect(this.addItem$, (oldState, itemName) => {
const newItem = {
id: uuid(), // unique hash generation fn()
name: itemName,
};
return {
...oldState,
items: [...oldState.items, newItem],
};
});
}
}
If we want to update one item, we have to query the items
array first to get a hold of the item and then construct a new array again.
What about deleting an item? You get the picture. It´s a lot of code, and it will grow even more if we have several types of collections in our state.
Using @ngrx/entity
Now let us see how our code will look when using @ngrx/entity
.
interface Item {
string;
string;
}
interface ComponentState extends EntityState<Item> {
loading: boolean;
}
const adapter: EntityAdapter<Item> = createEntityAdapter<Item>({
selectId: (item: Item) => item.id,
});
The entity adapter needs a selectId
function which is used to query items by id
within the collection.
Now let's see how the component has changed:
- Class Based (Classic)
- Functional Creation (NEW)
@Component({
selector: 'my-component',
})
export class MyComponent extends RxState<ComponentState> {
readonly addItem$ = new Subject<string>();
constructor() {
super();
this.connect(this.addItem$, (oldState, itemName) => adapter.addOne({ id: uuid(), name: itemName() }, oldState));
}
}
@Component({
selector: 'my-component',
})
export class MyComponent {
readonly #state = rxState<ComponentState>();
readonly addItem$ = new Subject<string>();
constructor() {
this.#state.connect(this.addItem$, (oldState, itemName) => adapter.addOne({ id: uuid(), name: itemName() }, oldState));
}
}
The addOne()
function is just one of many functions that help us manipulate the collection.
Delete an item? removeOne(item.id, oldState)
.
Check out the full list of adapter collection methods
Selecting state with @ngrx/entity
The entity adapter comes with a small set of default selectors we can use right out of the box.
- Class Based (Classic)
- Functional Creation (NEW)
import { select } from '@ngrx/store';
const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors();
@Component({
selector: 'my-component',
})
export class MyComponent extends RxState<ComponentState> {
readonly items$ = this.select(select(selectAll));
constructor() {
super();
}
}
import { select } from '@ngrx/store';
const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors();
@Component({
selector: 'my-component',
})
export class MyComponent {
readonly #state = rxState<ComponentState>();
readonly items$ = this.#state.select(select(selectAll));
}