Computed values are values that can be derived from the existing state or other computed values. Conceptually, they are very similar to formulas in spreadsheets. Computed values can't be underestimated, as they help you to make your actual modifiable state as small as possible. Besides that they are highly optimized, so use them wherever possible.
@StateRepository()
@State<OrderLineModel>({
name: 'orderLine',
defaults: {
price: 0,
amount: 1
}
})
@Injectable()
class OrderLineState extends NgxsDataRepository<OrderLineModel> {
@Computed()
public get total(): number {
return this.snapshot.price * this.snapshot.amount;
}
}
Computed values are automatically derived from your state if current state value that affects them changes. Computed values can be optimized away in many cases by NGXS as they are assumed to be pure. For example, a computed property won't re-run if none of the data used in the previous computation changed.
@StateRepository()
@State({
name: 'price',
defaults: 10
})
@Injectable()
class PriceState extends NgxsDataRepository<number> {
public setPrice(value: string): void {
this.setState(parseFloat(value));
}
}
@StateRepository()
@State({
name: 'amount',
defaults: 20
})
@Injectable()
class AmountState extends NgxsDataRepository<number> {
constructor(private readonly price: PriceState) {
super();
}
@Computed()
public get sum(): number {
return this.snapshot + this.price.snapshot;
}
public setAmount(value: string): void {
this.setState(parseFloat(value));
}
}
@Component({
selector: 'app',
template: `
<input placeholder="Price" [ngModel]="price.snapshot" (ngModelChange)="price.setPrice($event)" /> <br />
<input placeholder="Amount" [ngModel]="amount.snapshot" (ngModelChange)="amount.setAmount($event)" />
<p>Sum: {{ amount.sum }}</p>
`
})
class AppComponent {
constructor(public price: PriceState, public amount: AmountState) {}
}
@StateRepository()
@State<string[]>({
name: 'animals',
defaults: ['panda', 'horse', 'bee']
})
@Injectable()
export class ZooState extends NgxsDataRepository<string[]> {
@Computed()
public get pandas(): string[] {
return this.snapshot.filter((animal) => animal === 'panda');
}
@Computed()
public get horses(): string[] {
return this.snapshot.filter((animal) => animal === 'horse');
}
@Computed()
public get bees(): string[] {
return this.snapshot.filter((animal) => animal === 'bee');
}
}
By default selectors in NGXS are bound to a state. Sometimes you need the ability to join to un-related states in a high-performance re-usable fashion. A meta selector is a selector allows you to bind N number of selectors together to return a state stream. Let's say we have 2 states; 'zoos' and 'theme parks'. We have a component that needs to show all the zoos and theme parks for a given city. These are two very distinct state classes that are likely not related in any manner. We can use a meta selector to join these two states together like:
@StateRepository()
@State({ name: 'zoo', defaults: [] })
@Injectable()
export class ZooState extends NgxsDataRepository<string[]> {}
@StateRepository()
@State({ name: 'themePark', defaults: [] })
@Injectable()
export class ThemeParkState extends NgxsDataRepository<string[]> {}
@StateRepository()
@State({ name: 'city', defaults: [] })
@Injectable()
export class CityState extends NgxsDataRepository<string[]> {
constructor(private zoo: ZooState, private themePark: ThemeParkState) {
super();
}
@Computed()
public get zooThemeParks(): Observable<string[]> {
return combineLatest([this.zoo.state$, this.themePark.state$]).pipe(
map(([zoo, themeParks]) => [...zoo, ...themeParks])
);
}
}
Unlike NGXS native meta selectors, now you get a well typings by infer types when use combined selectors.
forkJoin
- When all observables complete, emit the last emitted value from each.
combineLatest
- When any observable emits a value, emit the latest value from each.
Usage is pretty similar, but you shouldn't forget to unsubscribe from combineLatest unlike forkJoin.
If you use any unknown value inside computed fields then they are not tracked:
@Injectable()
class MyFirstCountService {
private values$: BehaviorSubject<number> = new BehaviorSubject(0);
public increment(): void {
this.values$.next(this.getValue() + 1);
}
public getValue(): number {
return this.values$.getValue();
}
}
@StateRepository()
@State({
name: 'count',
defaults: 0
})
@Injectable()
class MySecondCountState extends NgxsDataRepository<number> {
constructor(private readonly first: MyFirstCountService) {
super();
}
@Computed()
public get sum(): number {
return this.snapshot + this.first.getValue();
}
@DataAction()
public increment(): void {
this.ctx.setState((state: number) => ++state);
}
}
@Component({
selector: 'app',
template: `
<button (click)="firstCount.increment()">Increment firstCount</button> <br />
<button (click)="secondCount.increment()">Increment secondCount</button>
<p>Sum: {{ secondCount.sum }}</p>
`
})
class AppComponent {
constructor(public firstCount: MyFirstCountService, public secondCount: MySecondCountState) {}
}
When you click the "Increment firstCount"
button, you will see that the sum
is not recalculated. Why? Everything is
very simple, caching occurs when any of the states has not changed and the result is still returned from the cache. In
order to help recalculate the value again, if you need it, you will need to manually update the cache:
@Injectable()
class MyFirstCountService {
private values$: BehaviorSubject<number> = new BehaviorSubject(0);
constructor(private readonly sequence: NgxsDataSequence) {}
public increment(): void {
this.values$.next(this.getValue() + 1);
this.sequence.updateSequence();
}
public getValue(): number {
this.values$.getValue();
}
}
sequence.updateSequence()
- This is necessary if you know that your states use values from third-party services, and
not the NGXS states in computed fields.