State management approaches in Angular
Published: 6/7/2024TODO: Write about NgRx
Angular BehaviorSubject on a service#
With parallel RXJS Observables.
The BehaviorSubject is private, but it’s current value can be read or updated using a getter or a setter, the stream can also be observed.
// Private BehaviorSubject initialised null
private _selectedForm = new BehaviorSubject<string | null>(null);
// Method to get the observable for subscription
get selectedForm$(): Observable<string | null> {
return this._selectedForm.asObservable();
}
// Getter to access the current value
get selectedForm(): string | null {
return this._selectedForm.value;
}
// Setter to update the value
set selectedForm(value: string | null) {
this._selectedForm.next(value);
}
# Component state from external inputs with internal management
###### Note: I refactored away from this pattern in the current app/lib
- It was much cleaner to have the list component directly read the query params and manage state internally from them, rather than have the parent (calling component) pass them in as inputs
- That was the case for the current application, as query strings will be used a lot
###### The component
- External inputs accept props from the parent (calling component)
- The external inputs are listened to in the constructor, updating the internal inputs
- The state is computed from the internal inputs
- Furthermore, state fed to observable to keep data in sync
```ts
// external inputs
pageInput = input<number>(0, { alias: 'page' });
limitInput = input<number>(0, { alias: 'limit' });
searchInput = input('', { alias: 'search' });
// internal inputs
pageState = signal<number>(this.pageInput());
limitState = signal<number>(this.limitInput());
searchState = signal<string>(this.searchInput());
// Computed state based on inputs
state = computed(() => ({
page: this.pageState(),
limit: this.limitState(),
search: this.searchState(),
}));
// selectors
public page = computed(() => this.state().page);
public limit = computed(() => this.state().limit);
public search = computed(() => this.state().search);
constructor() {
// Watch the input signal and update the internal state signal
effect(
() => {
this.familyIdState.set(this.familyIdInput());
this.pageState.set(this.pageInput());
this.limitState.set(this.limitInput());
this.searchState.set(this.searchInput());
},
{ allowSignalWrites: true }
);
}
Furthermore, separate the data from the above state. While having state changes trigger data updates.
// data access based on state
private data = toSignal(
toObservable(this.state).pipe(
filter(({ page, limit }) => page > 0 && limit > 0),
switchMap((state) =>
this.plantsService
.list$(state.byFamilyId, state.page, state.limit, state.search)
.pipe(
catchError((error) => {
console.error('Error fetching plants:', error);
return of(null); // Or handle error as needed
})
)
)
)
);
public plants = computed(() => this.data()?.plants);
public pages = computed(() => this.data()?.pages);
Parent component#
<lib-plant-list
[page]="pageParam"
[limit]="limitParam"
[search]="searchParam"
></lib-plant-list>
// query params
pageParam = 1;
limitParam = 12;
searchParam = '';
constructor(private route: ActivatedRoute) {
this.route.queryParams.subscribe((params) => {
this.familyIdParam = params['familyId'] ?? this.familyIdParam;
this.pageParam = params['plantPage'] ?? this.pageParam;
this.limitParam = params['limit'] ?? this.limitParam;
this.searchParam = params['search'] ?? this.searchParam;
});
}
Component state from external inputs with internal management#
Note: I refactored away from this pattern in the current app/lib#
- It was much cleaner to have the list component directly read the query params and manage state internally from them, rather than have the parent (calling component) pass them in as inputs
- That was the case for the current application, as query strings will be used a lot
The component#
- External inputs accept props from the parent (calling component)
- The external inputs are listened to in the constructor, updating the internal inputs
- The state is computed from the internal inputs
- Furthermore, state fed to observable to keep data in sync
// external inputs
pageInput = input<number>(0, { alias: 'page' });
limitInput = input<number>(0, { alias: 'limit' });
searchInput = input('', { alias: 'search' });
// internal inputs
pageState = signal<number>(this.pageInput());
limitState = signal<number>(this.limitInput());
searchState = signal<string>(this.searchInput());
// Computed state based on inputs
state = computed(() => ({
page: this.pageState(),
limit: this.limitState(),
search: this.searchState(),
}));
// selectors
public page = computed(() => this.state().page);
public limit = computed(() => this.state().limit);
public search = computed(() => this.state().search);
constructor() {
// Watch the input signal and update the internal state signal
effect(
() => {
this.familyIdState.set(this.familyIdInput());
this.pageState.set(this.pageInput());
this.limitState.set(this.limitInput());
this.searchState.set(this.searchInput());
},
{ allowSignalWrites: true }
);
}
Furthermore, separate the data from the above state. While having state changes trigger data updates.
// data access based on state
private data = toSignal(
toObservable(this.state).pipe(
filter(({ page, limit }) => page > 0 && limit > 0),
switchMap((state) =>
this.plantsService
.list$(state.byFamilyId, state.page, state.limit, state.search)
.pipe(
catchError((error) => {
console.error('Error fetching plants:', error);
return of(null); // Or handle error as needed
})
)
)
)
);
public plants = computed(() => this.data()?.plants);
public pages = computed(() => this.data()?.pages);
Parent component#
<lib-plant-list
[page]="pageParam"
[limit]="limitParam"
[search]="searchParam"
></lib-plant-list>
// query params
pageParam = 1;
limitParam = 12;
searchParam = '';
constructor(private route: ActivatedRoute) {
this.route.queryParams.subscribe((params) => {
this.familyIdParam = params['familyId'] ?? this.familyIdParam;
this.pageParam = params['plantPage'] ?? this.pageParam;
this.limitParam = params['limit'] ?? this.limitParam;
this.searchParam = params['search'] ?? this.searchParam;
});
}