Skip to main content

Getting Started

Create a State Instance

The new functional creation API lets you create and configure RxState in only one place.

Migration Guide

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

RxStates 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

info

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.

  1. Your input property is mutated from withing your own component (see example below)
  2. 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

tip

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.

tip

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

tip

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.

main.ts
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.

queueScheduler

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.

src: (https://rxjs.dev/api/index/const/queueScheduler)

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:

queue-scheduler.example.ts
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:

main.ts
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.

main.ts
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.

default-accumulator.ts
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.

main.ts
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)

deprecated

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.

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: [] });
}
}

Manage side effects

info

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.

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))));
}
}

Inheritance

info

The new rxState creation function does not return a class instance, thus no longer supporting inheritance.

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();
}
}

Disclaimer: this doc is work in progress. Not every use case has found its way into the docs. We encourage you to contribute 🙂.