Skip to main content

Concepts and best practices

Component shell and folder

  • @Input bindings are setters
  • @Output bindings are state derivations
  • The state is injected over the constructor
  • The state is displayed over a pipe in the template
  • The UI interaction is implemented over Subjects
  • use *rxLet over *ngIf

Bad:

<ng-container *ngIf="obj$ | async as obj"> {{ obj }} </ng-container>

Good:

<ng-container *rxLet="obj$ as obj"> {{ obj }} </ng-container>

Component implementation approach

Defining the interface

In a first step you want to setup the state interface. A property that should change the view of your component should find its place in the interface. View bindings and triggers, which in turn mutate your state, should be Subjects. In the best case, you keep your state normalized. Derived state should be handled separately.

Example view interface:

interface MyState {
items: string[];
listExpanded: boolean;
sortKey: string;
isAsc: boolean;
}

interface MyView {
click$: Subject<MouseEvent>();
expanded$: boolean;
vm$: Observable<MyState>; // ViewModel
}

Setup view interactions

@Component({
selector: 'app-stateful-component',
template: ` <div>{{ vm$ | async | json }}</div> `,
changeDetection: ChangeDetection.OnPush,
providers: [RxState],
})
export class StatefulComponent implements MyView {
readonly vm$: Observable<MyState> = this.state.select();
readonly click$ = new Subject<MouseEvent>();
readonly expanded$ = this.click$.pipe(); // map it

@Input('items') set items(items: string[]) {
this.state.set({ items });
}

constructor(private state: RxState<MyState>) {}
}
  • Hook up @Input bindings
@Input()
set task(task: Task) {
this.state.setState({task})
}
  • Hook up UI state
vm$ = this.state.select();
<ng-container *rxLet="obj$; let obj"> {{obj}} </ng-container>
  • Hook up UI interaction
<button (click)="btnReset$.next($event)">Reset</button>
  • Hook up @Output bindings

    @Output()
    taskChanges = this.state.$.pipe(distinctUntilKeyChanged('prop'));

Observables and projection functions are named in a way that gives us information about the returned data structure.

Think in Source => transform => state/effect

Bad:

getSearchResults() => this.inputChange$.pipe(
switchMap((q) => fetchData(q))
)

this.inputChange$.pipe(
this.toSearchResult
)

toSearchResults(o) => o.pipe(
map((data) => {
switchMap((q) => fetchData(q))
})
)