Skip to main content

How to structure a component around RxState

Signals first

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 a connect for an Observable source).
  • Outputs are state derivations: a computed() or a selected Observable.
  • The state handle is created with rxState() and held in a private field.
  • The template reads state through signals: state.signal('key') and computed().
  • UI interactions are modelled as Subjects (or rxActions) 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

The "reactive discipline" rationale (why interactions become Subjects/actions and side effects go through rxEffects rather than ad-hoc subscriptions) is covered in the RxState reactive discipline & effects concept.