How to structure a component around RxState
For local component state, Angular signals are the default — reach for signal(), computed(), and linkedSignal() first. RxState complements signals; it earns its place for global/shared state, complex derived state, and async-heavy orchestration (multi-source connect, actions, effects) bridged into signals — not as a replacement for signals. See Reactive state: global vs local, RxState + signals.
Goal
Apply a consistent architecture when a component's state is complex or async-heavy enough to warrant rxState(). Treat these as conventions rather than hard rules. For the why behind local-vs-global state, see the Reactive state: global vs local concept.
The shell
A component built around RxState follows a small set of conventions:
- Inputs feed state: a signal
input()(or aconnectfor anObservablesource). - Outputs are state derivations: a
computed()or a selectedObservable. - The state handle is created with
rxState()and held in a private field. - The template reads state through signals:
state.signal('key')andcomputed(). - UI interactions are modelled as
Subjects (orrxActions) that feed back into state.
Define the state interface first
A property that should change the view belongs in the state interface. Keep state normalized; handle derived state separately (with computed()), not by storing it.
interface MyState {
items: string[];
listExpanded: boolean;
sortKey: string;
isAsc: boolean;
}
Wire inputs, state, and the view
Create the state with rxState(), feed it from signal inputs, and expose a signal-based view. No providers: [RxState], no constructor DI, no | async.
import { Component, computed, input } from '@angular/core';
import { rxState } from '@rx-angular/state';
@Component({
selector: 'app-stateful-component',
template: `
@if (vm(); as vm) {
<div>{{ vm.items.length }} items ({{ vm.sortKey }})</div>
}
<button (click)="reset.next()">Reset</button>
`,
})
export class StatefulComponent {
// Inputs feed state
readonly items = input<string[]>([]);
// UI interactions are Subjects
readonly reset = new Subject<void>();
private readonly state = rxState<MyState>(({ set, connect }) => {
set({ items: [], listExpanded: false, sortKey: 'name', isAsc: true });
connect('items', toObservable(this.items));
connect(this.reset, () => ({ items: [], listExpanded: false }));
});
// The view is a signal, not an async pipe
readonly vm = this.state.computed(({ items, sortKey }) => ({
items: items(),
sortKey: sortKey(),
}));
// Extra derived values are computed() signals off the view model
readonly itemCount = computed(() => this.vm().items.length);
}
State is updated with set(...); there is no setState method on RxState.
Name transformations by their data shape
Name observables and projection functions so they describe the returned data structure, and think in source → transform → state/effect. Extract the transform into a named projection instead of inlining a switchMap:
private readonly searchResults$ = this.inputChange$.pipe(
this.toSearchResults,
);
private readonly toSearchResults = (o$: Observable<string>) =>
o$.pipe(switchMap((query) => this.api.search(query)));
Result
The component reads as: inputs in, one state handle, a signal view out. Change detection stays local and glitch-free because the view is a signal graph.
See also
- Reference:
RxState(functional) - How-to: Derive a view model
- Concept: Reactive state: global vs local, RxState + signals
The "reactive discipline" rationale (why interactions become
Subjects/actions and side effects go throughrxEffectsrather than ad-hoc subscriptions) is covered in the RxState reactive discipline & effects concept.