Angular Logo

Reactive State

How to manage state in Angular - the reactive way

Philipp Kief | 2023

What is a State? 🧐

State Function

State changes are produced by

  • User Interaction
  • Data loaded from Backend
  • Component Exchange (Routing)
  • Internationalization
  • Authorization
  • Theming
  • ...

Angular State Management

Local State Component State
Local State
NgRx Store Logo NgRx Store
Rxjs Logo Reactive State
Global State

Local State Example

export class AppComponent {
  todos: Todo[] = [];

  addTodo(description: string) {
    this.todos.push({ completed: false, description });
  }

  setTodoCompleteState(todo: Todo, completed: boolean) {
    this.todos = this.todos.map(t => {
      if (t.id === todo.id) {
        todo.completed = completed;
      }
      return t;
    });
  }
}
				  
				

Characteristics

  • Component specific
  • Angular out-of-the-box
  • Influenceable via property and event binding
  • Very limited

NgRx Store

Actions


  export class AddTodo implements Action {
    readonly type = ADD_TODO;
    constructor(public payload: any) {}
  }

  export class RemoveTodo implements Action {
    readonly type = REMOVE_TODO;
    constructor(public payload: any) {}
  }
								
							  

Dispatching Actions


  addTodo(event: Todo) {
    this.store.dispatch(new AddTodo(event));
  }
								
							  

Reducers


  export function reducer(
    state = initialState, action: TodossAction
  ): TodosState {
    switch (action.type) {
      case ADD_TODO: {
        const todo = action.payload;
        const todos = [...state.todos, todo];
        return { ...state, todos };
      }
    }
    return state;
  }
								
							  

Selectors


  import { createSelector } from '@ngrx/store';
    
  interface AppState { feature: FeatureState }
  
  const getTodos = (state: FeatureState) => state.todos;
  const getFeatureState = (state: AppState) => state.feature;
  const getFeatureStateTodos = createSelector(
    getFeatureState, 
    getTodos
  );
								
							  

Call Selector in Component


  export class TodosComponent implements OnInit {
    todos$: Observable<Todo[]>;

    constructor(private store: Store<TodosState>) {}

    ngOnInit() {
      this.todos$ = this.store.select(getTodos);
    }
  }
		   

Characteristics

  • Uniform state changes
  • clarity and transparency over the state
  • easy to test
  • strong influence on architecture
  • much additional code

Reactive State

Angular Service Layer

Angular Logo Store Service
Angular Logo Business Service
Angular Logo Adapter Service
Angular Logo Dialog Component(s)

Trigger State Change

Request new data from backend

Response with new data

Save new data in store

Inform dialog component

Abstract Store Class

  • Parent class of all Store classes
  • Provides update logic
  • Provides state observable
  • Provides selector

Abstract Store Class

export abstract class Store<State extends object> {
  private readonly actionSource: Subject<Action<State>>;
  private readonly stateSource: BehaviorSubject<State>;
  readonly state$: Observable<State>;

  constructor(initialState: State) {
    this.stateSource = new BehaviorSubject<State>(
      initialState
    );
    this.state$ = this.stateSource.asObservable();
    this.actionSource = new Subject<Action<State>>();

    this.actionSource.pipe(
      observeOn(queueScheduler)
    ).subscribe(action => {
      const currentState = this.stateSource.getValue();
      const nextState = action(currentState);
      this.stateSource.next(nextState);
    });
  }

  select<SelectedState>(
    selector: (state: State) => SelectedState
  ): Observable<SelectedState> {
    return this.state$.pipe(
      map(selector),
      map(state => structuredClone(state)),
      distinctUntilObjectChanged()
    );
  }

  update(stateChanger: (state: State) => State) {
    this.actionSource.next(stateChanger);
  }
}
						 

Feature Store Class

  @Injectable({
    providedIn: 'root',
  })
  export class TodoStoreService extends Store<TodoStoreState> {
    constructor() {
      super('TodoStore', { todos: [] });
    }

    addTodo(todo: Todo) {
      this.update(state => ({
        ...state,
        todos: [
          ...state.todos,
          todo
        ],
    }));
  }
		   

Dialog Component

  export class TodoComponent {
    readonly allTodos$: Observable<Todo[]>;

    constructor(
      private todoService: TodoService,
      private todoStore: TodoStoreService
    ) {
      this.allTodos$ = this.todoStore.select(
        state => state.todos
      );
    }

    addTodo(description: string) {
      this.todoService.addTodo(description);
    }
  }
		   

Business Service

  export class TodoService {
    
    constructor(
      private todoAdapter: TodoAdapterService,
      private todoStore: TodoStoreService
    ) { }

    async addTodo(description: string) {
      const todo = await lastValueFrom(
        this.todoAdapter.addTodo(description)
      );
      this.todoStore.addTodo(todo);
    }
  }
		   

Characteristics

  • Uniform state changes
  • Fits into the modul's service layer
  • No additional dependency
  • Modular state
  • Customizable

Demo

Link to playground

Similar libraries