DEV Community

Maxim Pavlov
Maxim Pavlov

Posted on

State-driven development of user interface components

I often see code describing the state of the user interface that needs to be simplified.

Let's see code that outputs a list of users.

  <ng-container *ngIf="isLoading && !error">Loading...</ng-container>
  <ul *ngIf="users && users.length && !error">
    <li *ngFor="let user of users">{{}}</li>
  <ng-container *ngIf="!error && !loading && users && !users.length">Nothing found</ng-container>
  <ng-container *ngIf="!isLoading && error">{{error.message}}</ng-container>

This code is just awful. It is difficult to read and maintain.
I prefer another way. I used to read about the theory of finite-state machines. The state machine has a finite set of states, and it is in one of these states at each moment.

We have four states of the user's list:

  1. Loading
  2. Users loaded
  3. Users were loaded with errors
  4. Users were not founded

Let's describe the state with a discriminated union.

type State =
  | { status: 'loading' }
  | { status: 'success', data: IUser[] }
  | { status: 'failed', error: Error }
  | { status: 'not-founded' }

Let's rewrite view logic.

  <ng-container *ngIf="state.status === 'loading'">Loading...</ng-container>
  <ul *ngIf="state.status === 'success'">
    <li *ngFor="let user of">{{}}</li>
  <ng-container *ngIf="state.status === 'not-found'">Nothing found</ng-container>
  <ng-container *ngIf="state.status === 'failed'">{{state.error.message}}</ng-container>

You can make the state type universal by using generics.

type State<TSuccessData> =
  | { status: 'loading' }
  | { status: 'success', data: TSuccessData }
  | { status: 'failed', error: Error }
  | { status: 'not-founded' }
type UsersListState = State<IUser[]>;

This code is more reading and self-documenting. Presently your IDE gives better hints for you.
Code completion example
Code completion example
Your team will be grateful to you.

P.S. Sorry for my English. It is my first article in English.

Top comments (0)