Getting Started
Create a State Instance
The new functional creation API lets you create and configure RxState
in only one place.
Read the following section for a migration guide explaining how to upgrade your codebase to the new API.
import { rxState } from '@rx-angular/state';
import { RxFor } from '@rx-angular/template/for';
@Component({
template: `<movie *rxFor="let movie of movies$" [movie]="movie" />`,
imports: [RxFor],
})
export class MovieListComponent {
private state = rxState<{ movies: Movie[] }>(({ set }) => {
// set initial state
set({ movies: [] });
});
// select a property for the template to consume as an observable
movies$ = this.state.select('movies');
// OR select a property for the template to consume as a signal
movies = this.state.signal('movies'); // Signal<Movie[]>
}
The functional approach will be the new default approach for newer versions.
Read the Migration Guide for a migration guide explaining how to upgrade your codebase to the new API.
Connect global state
Connect state slices from third-party services (e.g. NgRx Store
)
Many people have problems combining observables with the component state in a clean way.
Here is a use case where the @ngrx/store
gets connected to the local state:
import { rxState } from '@rx-angular/state';
@Component({})
export class MovieListComponent {
private store = inject<Store<MovieState>>(Store);
private state = rxState<{ movies: Movie[] }>(({ set, connect }) => {
// set initial state
set({ movies: [] });
// connect global state to your local state
connect('movies', this.store.select('movies'));
// OR connect global state in form of a signal to your local state
connect('movies', this.store.selectSignal('movies'));
});
}
Store loading & error information
RxState
s API makes it extremely easy to derive and store loading & error information when interacting with 3rd party data.
Using one of the overloads of the connect
method, we can fill our whole state object with only one connection.
import { Component, inject } from '@angular/core';
import { rxState } from '@rx-angular/state';
import { RxFor } from '@rx-angular/template/for';
import { RxIf } from '@rx-angular/template/if';
import { map, catchError, startWith, endWith } from 'rxjs';
@Component({
template: `
<loader *rxIf="loading$" />
<error *rxIf="error$" />
<movie *rxFor="let movie of movies$" [movie]="movie" />
`,
imports: [RxFor, RxIf],
})
export class MovieListComponent {
private movieResource = inject(MovieResource);
private state = rxState<{
movies: Movie[];
loading: boolean;
error: boolean;
}>(({ set, connect }) => {
// set initial state
set({ movies: [], loading: false, error: false });
// connect global state to your local state
connect(
this.movieResource.fetchMovies().pipe(
// map actual data
map((movies) => ({ movies })),
// in case of an error, store it
catchError(() => of({ error: true })),
// start with loading true
startWith({ loading: true }),
// when request completes, we can set loading to false
endWith({ loading: false }),
),
);
});
// select a property for the template to consume
movies$ = this.state.select('movies');
loading$ = this.state.select('loading');
error$ = this.state.select('error');
}
Input Property Bindings
Combining Input
bindings passing single values with RxState
As change detection is anyway executed when a new value arrives as input binding, you don't need to wrap that property with an async pipe in your template.
This approach is only suggested to use certain use cases.
- Your input property is mutated from withing your own component (see example below)
- You need that property in your state to compute other values
import { rxState } from '@rx-angular/state';
import { Subject } from 'rxjs';
@Component({
selector: 'app-count',
template: `
<div>{{ count$ | async }}</div>
<button (click)="increment.next()">increment</button>
<button (click)="decrement.next()">decrement</button>
`,
})
export class CounterComponent {
readonly count$ = this.state.select('count');
@Input() set count(count: number) {
this.state.set({ count });
}
increment = new Subject();
decrement = new Subject();
private state = rxState<{ count: number }>(({ set, connect }) => {
// set initial state
set({ count: 0 });
// increment
connect('count', this.increment, ({ count }) => count++);
// decrement
connect('count', this.decrement, ({ count }) => count--);
});
}
Combining Input
bindings passing Observables
You can save 1 change detection run per emission and improve performance of your application by providing Observables
directly as Input
.
This way the ChangeDetection for the Input
binding will only fire once for the first assignment.
You can use coerceObservable
from @rx-angular/cdk/coercing
to support static values as well as Observables with a single line of code.
import { rxState } from '@rx-angular/state';
import { Observable, Subject } from 'rxjs';
import { coerceObservable } from '@rx-angular/cdk/coercing';
@Component({
selector: 'app-count',
template: `
<div>{{ count$ | async }}</div>
<button (click)="increment.next()">increment</button>
<button (click)="decrement.next()">decrement</button>
`,
})
export class CounterComponent {
readonly count$ = this.state.select('count');
@Input() set count(count: Observable<number> | number) {
// You can use `coerceObservable` from `@rx-angular/cdk` to support static values as well as Observables with a single line of code.
this.state.connect('count', coerceObservable(count));
}
increment = new Subject();
decrement = new Subject();
private state = rxState<{ count: number }>(({ set, connect }) => {
// set initial state
set({ count: 0 });
// increment
connect('count', this.increment, ({ count }) => count++);
// decrement
connect('count', this.decrement, ({ count }) => count--);
});
}
Output Property Bindings
Combining Output
bindings directly from RxState.
For output bindings it is recommended to use the $
property. The $
property exposes the raw state without any selector benefits as memoization.
This is important for output events, as events should not be stateful, e.g. repeat their latest value.
import { rxState } from '@rx-angular/state';
import { select } from '@rx-angular/state/selections';
import { Observable, Subject } from 'rxjs';
import { coerceObservable } from '@rx-angular/cdk/coercing';
@Component({
selector: 'app-count',
template: `
<div>{{ count$ | async }}</div>
<button (click)="increment.next()">increment</button>
<button (click)="decrement.next()">decrement</button>
`,
})
export class StatefulComponent {
readonly count$ = this.state.select('count');
@Input() set count(count: Observable<number> | number) {
// You can use `coerceObservable` from `@rx-angular/cdk` to support static values as well as Observables with a single line of code.
this.state.connect('count', coerceObservable(count));
}
@Output() countChange = this.state.$.pipe(select('count'));
increment = new Subject();
decrement = new Subject();
private state = rxState<{ count: number }>(({ set, connect }) => {
// set initial state
set({ count: 0 });
// increment
connect('count', this.increment, ({ count }) => count++);
// decrement
connect('count', this.decrement, ({ count }) => count--);
});
}
Updates based on previous state
Often it is needed calculate your new state based off some input and a previous state. The following example shows a
local filtering algorithm implemented with RxState
.
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { rxState } from '@rx-angular/state';
import { RxFor } from '@rx-angular/template/for';
@Component({
template: `
<input placeholder="Search" [formControl]="search" />
<movie *rxFor="let movie of movies$" [movie]="movie" />
`,
imports: [RxFor, ReactiveFormsModule],
})
export class MovieListComponent {
private store = inject<Store<MovieState>>(Store);
search = new FormControl<string>();
private state = rxState<{ movies: Movie[] }>(({ set, connect }) => {
// set initial state
set({ movies: [] });
// connect global state to your local state
connect('movies', this.store.select('movies'));
// use the oldState and the searchInput to calculate the new state
connect('movies', this.search.valueChanges, (oldState, searchInput) => {
return oldState.movies.filter((movie) => movie.title.includes(searchInput));
});
});
// select a property for the template to consume
movies$ = this.state.select('movies');
}
Derive state using selections
Instead of storing your derived state as properties in your state, use the selection APIs to derive them as new streams.
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { rxState } from '@rx-angular/state';
import { RxFor } from '@rx-angular/template/for';
@Component({
template: `
<input placeholder="Search" [formControl]="search" />
<movie *rxFor="let movie of filteredMovies$" [movie]="movie" />
`,
imports: [RxFor, ReactiveFormsModule],
})
export class MovieListComponent {
private store = inject<Store<MovieState>>(Store);
search = new FormControl<string>();
private state = rxState<{ movies: Movie[]; searchValue: string }>(({ set, connect }) => {
// set initial state
set({ movies: [], searchValue: string });
// connect global state to your local state
connect('movies', this.store.select('movies'));
// use the oldState and the searchInput to calculate the new state
connect('searchValue', this.search.valueChanges);
});
// derive filteredMovies$ from your stored state as an observable
filteredMovies$ = this.state.select(['movies', 'searchValue'], (movies, searchValue) => {
return movies.filter((movie) => movie.title.includes(searchValue));
}); // Observable<Movie[]>
// derive filteredMovies from your stored state as a signal
filteredMovies = this.state.computed(({ movies, searchValue }) => {
return movies().filter((movie) => movie.title.includes(searchValue()));
}); // Signal<Movie[]>
// derive asynchronous filteredMovies from your stored state as a signal
filteredMovies = this.state.computedFrom(
select('searchValue'),
switchMap((searchValue) => this.movieResource.fetchMovies(searchValue)),
startWith([] as Movie[]), // needed as the initial value otherwise it will throw an error
); // Signal<Movie[]>
}
rxState in a Service
If you strive for a more sophisticated separation of concerns, you can simply use rxState
as part of any @Injectable
service.
import { inject, Injectable } from '@angular/core';
import { rxState } from '@rx-angular/state';
@Injectable({ providedIn: 'root' })
export class MovieService {
private resource = inject(MovieResource);
readonly state = rxState<{ movies: Movie[] }>(({ set, connect }) => {
// set initial state
set({ movies: [] });
// connect global state to your local state
connect('movies', this.resource.fetchMovies());
});
// select a property for the template to consume as an observable
movies$ = this.state.select('movies'); // Observable<Movie[]>
// select a property for the template to consume as a signal
movies = this.state.signal('movies'); // Signal<Movie[]>
}
expose readOnly rxState from a Service
If you only want to expose your RxState
instance as a readonly state, you can use the new asReadOnly()
function.
This allows you to only expose APIs that allows consumers to read from your state. Write access remains private to the
owner of the RxState
instance.
import { inject, Injectable } from '@angular/core';
import { rxState } from '@rx-angular/state';
@Injectable({ providedIn: 'root' })
export class MovieService {
private resource = inject(MovieResource);
private readonly _state = rxState<{ movies: Movie[] }>(({ set, connect }) => {
// set initial state
set({ movies: [] });
// connect global state to your local state
connect('movies', this.resource.fetchMovies());
});
// consumers can use `get`, `select`, `signal` and `compute`
readonly state = this._state.asReadOnly();
}
rxState as DI Token
You can use the rxState
function as a factory for an InjectionToken
. This way you can still create DI State tokens.
Create a local Service
by using rxState
as factory function.
import { InjectionToken, inject } from '@angular/core';
import { rxState } from '@rx-angular/state';
export const MovieState = new InjectionToken({
factory: () => {
// inject dependencies here
const movieResource = inject(MovieResource);
return rxState<{ movies: Movie[] }>(({ set, connect }) => {
// set initial state
set({ movies: [] });
// connect global state to your local state
connect('movies', movieResource.fetchMovies());
});
},
});
Provide
the Service
inside the providers array when using a Component
or Directive
.
@Component({
template: ` <div>{{ viewState$ | async | json }}</div> `,
providers: [MovieState],
})
export class MovieListComponent {
private movieState = inject(MovieState);
viewState$ = this.movieState.select('movies');
}
Configuration
There are a couple of settings you can adjust when using RxState
.
provideRxStateConfig
Configurations for RxState
instances are provided in the DI tree by using the provideRxStateConfig
provider function.
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { provideRxStateConfig } from '@rx-angular/state';
bootstrapApplication(AppComponent, {
providers: [
provideRxStateConfig(),
/* define features here */
],
});
Scheduler
By default, RxState
observes changes and computes new states on the queueScheduler
. You can modify
this behavior by using the withScheduler()
or withSyncScheduler()
configuration features.
The queueScheduler
provides a certain level of integrity, as state mutations that cause other state mutations are executed in the right order.
When used without delay, it schedules given task synchronously - executes it right when it is scheduled. However when called recursively, that is when inside the scheduled task, another task is scheduled with queue scheduler, instead of executing immediately as well, that task will be put on a queue and wait for current one to finish.
This means that when you execute task with queue scheduler, you are sure it will end before any other task scheduled with that scheduler will start.
This means, it is possible that u run into the situation where you mutate the state, but isn't synchronous.
See the following very simplified example:
import { rxState } from '@rx-angular/state';
const state = rxState<{ foo: string; bar: string }>();
state.set(() => {
// will execute after the { bar: 'bar' } was set
state.set('foo', 'foo');
console.log(state.get('foo')); // prints undefined
// will execute first
return {
bar: 'bar',
};
});
In order to escape this behavior, you can define the scheduling to be fully synchronous:
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { provideRxStateConfig, withSyncScheduler } from '@rx-angular/state';
bootstrapApplication(AppComponent, {
providers: [provideRxStateConfig(withSyncScheduler())],
});
It is however also possible to define whatever SchedulerLike
you want, e.g. make your state asynchronous by using the asapScheduler
.
import { bootstrapApplication } from '@angular/platform-browser';
import { asapScheduler } from 'rxjs';
import { AppComponent } from './app.component';
import { provideRxStateConfig, withScheduler } from '@rx-angular/state';
bootstrapApplication(AppComponent, {
providers: [
provideRxStateConfig(
/* use the asapScheduler to new states -> makes the state async! */
withScheduler(asapScheduler),
),
],
});
Accumulator
The accumulator defines how state transitions from change to change and how slices are integrated into the state.
By default, RxState
operators immutable on the top level of the state. Deeply nested objects are shallow cloned on state changes.
In order to adjust this behavior or add new functionality, you can define your own AccumulatorFn
. This enables you to e.g. integrate an immer
based
state management.
The AccumulationFn
is a function that runs on every state change and is responsible for building a new state from a given input.
By default it merges together the state by spreading it - producing a new object on every change.
import { AccumulationFn } from '@rx-angular/state/selections';
const defaultAccumulator: AccumulationFn = <T>(state: T, slice: Partial<T>): T => {
return { ...state, ...slice };
};
withAccumulator
Use the withAccumulator
configuration feature to set a custom AccumulatorFn
via the DI tree.
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { provideRxStateConfig, withAccumulator } from '@rx-angular/state';
import { produce } from 'immer';
const immerAccumulator = (state, slice) =>
produce(state, (draft) => {
Object.keys(slice).forEach((k) => {
draft[k] = slice[k];
});
});
bootstrapApplication(AppComponent, {
providers: [
provideRxStateConfig(
/* use the asapScheduler to new states -> makes the state async! */
withAccumulator(immerAccumulator),
),
],
});
(deprecated) Custom state accumulation (mutability)
The setAccumulator
API is deprecated in favor of the withAccumulator
configuration feature
Use setAccumulator
to change that behavior. This way you could e.g. introduce immer
as your accumulation to have
full immutability.
import { produce } from 'immer';
import { rxState } from '@rx-angular/state';
const immerAccumulator = (state, slice) =>
produce(state, (draft) => {
Object.keys(slice).forEach((k) => {
draft[k] = slice[k];
});
});
state = rxState(({ setAccumulator }) => setAccumulator(immerAccumulator));
Or you can use any other custom deep-copying algorithm or simply go fully mutable.
const myAccumulator = (state: MyState, slice: Partial<MyState>) => deepCopy(state, slice);
state.setAccumulator(myAccumulator);
This can be done at runtime.
Migrate to new functional API
The new functional API provides a nicer developer experience and aligns with the new Angular APIs recently released.
It will be the new default approach for using RxState
and we want to emphasize everyone to use the new functional API.
The following examples showcases the key differences and how to migrate from the class based approach to the functional one.
Providers
The beauty of the new functional approach is that it works without providers. This way, you simply use the new
creation function rxState
.
Instead of importing RxState
and putting it into the providers
array, you now import rxState
.
The namespace still stays the same.
- Class Based (Classic)
- Functional Creation (NEW)
import { RxState } from '@rx-angular/state/state';
import { inject, Component } from '@angular/core';
@Component({
template: `<div>{{ state$ | async | json }}</div>`,
providers: [RxState],
})
export class MovieListComponent {
// expose state for the template
readonly viewState$ = this.state.select();
constructor(private state: RxState<{ movies: Movie[] }>) {
this.state.set({ movies: [] });
}
}
import { rxState } from '@rx-angular/state';
import { Component } from '@angular/core';
@Component({
template: `<div>{{ state$ | async | json }}</div>`,
})
export class MovieListComponent {
// create `RxState` in a single step, no providers anymore
private state = rxState<{ movies: Movie[] }>();
// expose state for the template
readonly state$ = this.state.select();
constructor() {
// use the state APIs as before
this.state.set({ movies: [] });
}
}
Manage side effects
The new rxState
creation function drops the hold
method and with it, it's capabilities of managing side effects.
If you need to have such a feature, we encourage to use rxEffects
.
- Class Based (Classic)
- Functional Creation (NEW)
import { RxState } from '@rx-angular/state';
import { Component } from '@angular/core';
import { Subject } from 'rxjs';
@Component({
template: `<movie (click)="deleteClick$.next(item.id)" *rxFor="let item of items$" />`,
providers: [RxState],
})
export class MovieListComponent {
readonly items$ = this.state.select('movies');
readonly deleteClick$ = new Subject();
constructor(
private state: RxState<{ movies: Movie[] }>,
private movieResource: MovieResource,
) {
this.state.hold(this.deleteClick$.pipe(concatMap((id) => this.movieResource.delete(id))));
}
}
Not support anymore, please use rxEffects
.
Inheritance
The new rxState
creation function does not return a class instance, thus no longer supporting inheritance.
- Class Based (Classic)
- Functional Creation (NEW)
If you wish, there is also the possibility of extending the RxState
service. This can come in very handy for small
components. Keep in mind you will expose the full RxState
API to everyone having access to the component extending it.
import { RxState } from '@rx-angular/state';
@Directive({
selector: '[appStateful]',
})
export class StatefulComponent extends RxState<{ state: number }> {
readonly state$ = this.select();
constructor() {
super();
}
}
Not support anymore, please choose either the RxState as a Service approach, or use it as a private property.
Disclaimer: this doc is work in progress. Not every use case has found its way into the docs. We encourage you to contribute 🙂.