Skip to main content

RxState

Overview

RxState is a light-weight reactive state management service for managing local state in Angular.

Example

Component({
selector: 'app-stateful',
template: `<div>{{ state$ | async | json }}</div>`,
providers: [RxState],
});
export class StatefulComponent {
readonly state$ = this.state.select();

constructor(private state: RxState<{ foo: string }>) {}
}

Signature

class RxState<T extends object> implements OnDestroy, Subscribable<T> {
readonly $: Observable<T> = this.accumulator.signal$;

connect(inputOrSlice$: Observable<Partial<T> | V>, projectFn?: ProjectStateReducer<T, V>) => void;
connect(key: K, slice$: Observable<T[K]>) => void;
connect(key: K, input$: Observable<V>, projectSliceFn: ProjectValueReducer<T, K, V>) => void;

set(stateOrProjectState: Partial<T> | ProjectStateFn<T>) => void;
set(key: K, projectSlice: ProjectValueFn<T, K>) => void;

select(op: OperatorFunction<T, A>) => Observable<A>;
select(k1: K1) => Observable<T[K1]>;
select(k: K, fn: (val: T[K]) => V): Observable<V>;
select(keys: K[], fn: (slice: PickSlice<T, K>) => V, keyCompareMap?: KeyCompareMap<Pick<T, K>>): Observable<V>;
select() => Observable<T>;

get() => T;
get(k1: K1) => Partial<T>;

hold(obsOrObsWithSideEffect: Observable<S>, sideEffectFn?: (arg: S) => void) => void;

setAccumulator(accumulatorFn: AccumulationFn) => void;
}

$ (state observable)

typeof: Observable<T>

The unmodified state exposed as Observable<T>. It is not shared, distinct or gets replayed. Use the $ property if you want to read the state without having applied stateful to it.


asReadOnly

typeof: Pick<RxState<State>, 'get' | 'select' | 'computed' | 'signal'>

Return RxState in ReadOnly mode that is exposing get(), select(), computed() and signal() methods. This can be helpful when you don't want others to write in your state.

const readOnlyState = state.asReadOnly();
const getNum = readOnlyState.get('num');
const selectNum$ = readOnlyState.select('num');

Trying to call any method that is not exposed in readOnlyState will throw an appropriate error

const readOnlyState = state.asReadOnly();
readOnlyState['set']('num', (state) => state.num + 1);
throwing -> readOnlyState.set is not a function

connect

Signature

connect(inputOrSlice$: Observable<Partial<T> | V>, projectFn?: ProjectStateReducer<T, V>): void

Connect an Observable<Partial<T>> to the state T. Any change emitted by the source will get merged into the state. Subscription handling is done automatically.

Example

const sliceToAdd$ = interval(250).pipe(mapTo({
bar: 5,
foo: 'foo'
});
state.connect(sliceToAdd$);
// every 250ms the properties bar and foo get updated due to the emission of sliceToAdd$

// Additionally you can provide a `projectionFunction` to access the current state object and do custom mappings.

const sliceToAdd$ = interval(250).pipe(mapTo({
bar: 5,
foo: 'foo'
});
state.connect(sliceToAdd$, (state, slice) => state.bar += slice.bar);
// every 250ms the properties bar and foo get updated due to the emission of sliceToAdd$. Bar will increase by
// 5 due to the projectionFunction

Signature

connect(key: K, slice$: Observable<T[K]>): void

Connect an Observable<T[K]> source to a specific property K in the state T. Any emitted change will update this specific property in the state. Subscription handling is done automatically.

Example

const myTimer$ = interval(250);
state.connect('timer', myTimer$);
// every 250ms the property timer will get updated

Signature

connect(key: K, slice$: Observable<V>, projectSliceFn: ProjectValueReducer<T, K, V>): void

Connect an Observable<V> source to a specific property in the state. Additionally you can provide a projectionFunction to access the current state object on every emission of your connected Observable. Any change emitted by the source will get merged into the state. Subscription handling is done automatically.

Example

const myTimer$ = interval(250);
state.connect('timer', myTimer$, (state, timerChange) => (state.timer += timerChange));
// every 250ms the property timer will get updated

set

Signature

set(stateOrProjectState: Partial<T> | ProjectStateFn<T>): void

Manipulate one or many properties of the state by providing a Partial<T> state or a ProjectionFunction<T>.

Example

// Update one or many properties of the state by providing a `Partial<T>`

const partialState = {
foo: 'bar',
bar: 5,
};
state.set(partialState);

// Update one or many properties of the state by providing a `ProjectionFunction<T>`

const reduceFn = (oldState) => ({
bar: oldState.bar + 5,
});
state.set(reduceFn);

Signature

set(key: K, projectSlice: ProjectValueFn<T, K>): void

Manipulate a single property of the state by the property name and a ProjectionFunction<T>.

Example

const reduceFn = (oldState) => oldState.bar + 5;
state.set('bar', reduceFn);

select

Signature

select(): Observable<T>

Returns the state as cached and distinct Observable<T>. This way you don't have to think about late subscribers, multiple subscribers or multiple emissions of the same value

Example

const state$ = state.select();
state$.subscribe((state) => doStuff(state));

Signature

select(op: OperatorFunction<T, A>): Observable<A>

Returns the state as cached and distinct Observable<A>. Accepts arbitrary rxjs operators to enrich the selection with reactive composition.

Example

const profilePicture$ = state.select(
map((state) => state.profilePicture),
switchMap((profilePicture) => mapImageAsync(profilePicture)),
);

Signature

select(k1: K1): Observable<T[K1]>

Access a single property of the state by providing keys. Returns a single property of the state as cached and distinct Observable<T[K1]>.

Example

// Access a single property

const bar$ = state.select('bar');

// Access a nested property

const foo$ = state.select('bar', 'foo');

Signature

select(k: K, fn: (val: T[K]) => V): Observable<V>;

Transform a single property of the state by providing a key and map function. Returns result of applying function to state property as cached and distinct Observable<V>.

Example

// Project state based on single property
const foo$ = state.select('bar', (bar) => `bar equals ${bar}`);

Signature

select(keys: K[], fn: (slice: PickSlice<T, K>) => V, keyCompareMap?: KeyCompareMap<Pick<T, K>>): Observable<V>;

Transform a slice of the state by providing keys and map function. Returns result of applying function to state slice as cached and distinct Observable<V>.

Example

// Project state slice
const text$ = state.select(['query', 'results'], ({ query, results }) => `${results.length} results found for "${query}"`);

get

Signature

get(): T

Read from the state in imperative manner. Returns the state object in its current state.

Example

const { disabled } = state.get();
if (!disabled) {
doStuff();
}

Signature

get(k1: K1): Partial<T>

Read from the state in an imperative manner by providing keys as parameters to reach deeply nested values. Returns the part of state object.

Example

interface State {
bar: { foo: `test` };
baz: true;
}

// Access a single property

const bar = state.get('bar');

// Access a nested property

const foo = state.get('bar', 'foo');

hold

Signature

  hold(obsOrObsWithSideEffect: Observable<S>, sideEffectFn?: (arg: S) => void): void

Manages side-effects of your state. Provide an Observable<any> side-effect and an optional sideEffectFunction. Subscription handling is done automatically.

Example

// Directly pass an observable side-effect
const localStorageEffect$ = changes$.pipe(tap((changes) => storeChanges(changes)));
state.hold(localStorageEffect$);

// Pass an additional `sideEffectFunction`

const localStorageEffectFn = (changes) => storeChanges(changes);
state.hold(changes$, localStorageEffectFn);

setAccumulator

Signature

setAccumulator(accumulatorFn: AccumulationFn) => void

Allows to customize state accumulation function. This can be helpful to implement deep updates and tackle other immutability problems in a custom way.

Example

const myAccumulator = (state: MyState, slice: Partial<MyState>) => ({
...state,
...slice,
});

this.state.setAccumulator(myAccumulator);