Skip to main content

How to manage global state

Author: @Phhansen

How to manage global state

For an explainer on how to easily identify what should be global state and what should local state, please read: Difference between Global and Local state.

Example

As with the global/local state snippet, we'll be doing the same example to-do app with 2 views.

To do

  • Renders a list of tasks that must be completed and a counter that shows how many tasks left to do.
  • The list can be expanded or collapsed and has property isExpanded.
  • Gets tasks array from endpoint tasks/get and filters out tasks that already answered.
interface TodosState {
tasks: Task[];
isExpanded: boolean;
}

@Component({
selector: 'todos',
templateUrl: './todo.component.html',
})
export class TodoComponent extends RxState<TodosState> {
readonly tasks$ = this.select('tasks');
readonly counter$ = this.select(
map((state) => state.tasks),
map((tasks) => tasks.length)
);
readonly isExpanded$ = this.select('isExpanded');

constructor(private tasksService: TasksService) {
super();

/* Filter out tasks that are done */
this.connect(
'tasks',
this.tasksService
.fetchTasks()
.pipe(filter((tasks) => tasks.filter((task) => !task.done)))
);
}
}

Setup

  • Renders a list of all existing tasks and a counter that shows the total amount of tasks.
  • The list can be expanded or collapsed and has property isExpanded.
  • Gets tasks as array from endpoint tasks/get.
interface AllTodosState {
tasks: Task[];
isExpanded: boolean;
}

@Component({
selector: 'all-tasks',
templateUrl: './all-tasks.component.html',
})
export class AllTasksComponent extends RxState<AllTodosState> {
readonly tasks$ = this.select('tasks');
readonly counter$ = this.select(
map((state) => state.tasks),
map((tasks) => tasks.length)
);
readonly isExpanded$ = this.select('isExpanded');

constructor(private tasksService: TasksService) {
super();

/* Fetch tasks from backend */
this.connect('tasks', this.tasksService.fetchTasks());
}
}

What is global and what is local?

Looking at the above examples, let us see what is local and what is global!

  • counter property is a part of local state of each view. The counter value is specific for each view.
  • isExpanded property is also part of local state. Both lists can be expanded/collapsed but this status isn't shared between them and they don't care about this status of each other.
  • tasks array is a part of our app global state. This array needed for each view and received from the same endpoint. We don't need to load it twice. It is time to introduce a global layer to our application and move tasks array and retrieving logic there.

Moving the tasks array to our global state

We can handle global state in different ways, but for this snippet we´re going to use an injectionToken.

import { InjectionToken } from '@angular/core';
import { RxState } from '@rx-angular/state';

export interface Task {
id: number;
label: string;
done: boolean;
}

export interface GlobalState {
tasks: Task[];
}

export const GLOBAL_RX_STATE = new InjectionToken<RxState<GlobalState>>(
'GLOBAL_RX_STATE'
);

We then provide the injectionToken in our app.module.ts.

import { GLOBAL_RX_STATE, GlobalState } from "./rx-state";
...

@NgModule({
imports: [...],
declarations: [...],
providers: [{
provide: GLOBAL_RX_STATE, useFactory: () => new RxState<GlobalState>()
}],
bootstrap: [...]
})
export class AppModule {}

We can then load the tasks in the AppComponent via our tasksService.fetchTasks() and just have our TodoComponent and AllTasksComponent connect to the global state.

import { GLOBAL_RX_STATE } from './rx-state';

@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
constructor(@Inject(GLOBAL_RX_STATE) private state, private tasksService: TasksService) {
/* Fetch tasks from backend */
this.state.connect("tasks", this.tasksService.fetchTasks());
}

And our updated TodoComponent

interface TodosState {
tasks: Task[];
isExpanded: boolean;
}

@Component({
selector: 'todos',
templateUrl: './todo.component.html',
})
export class TodoComponent extends RxState<TodosState> {
readonly tasks$ = this.select('tasks');
readonly counter$ = this.select(
map((state) => state.tasks),
map((tasks) => tasks.length)
);
readonly isExpanded$ = this.select('isExpanded');

constructor(
@Inject(GLOBAL_RX_STATE) private globalState: RxState<GlobalState>
) {
super();

/* Connect to global state and filter out already completed tasks */
this.connect(
'tasks',
this.globalState
.select('tasks')
.pipe(map((tasks) => tasks.filter((task) => !task.done)))
);
}
}

Here we connect to the global state instance and filter out the already completed tasks.

Our AllTasksComponent is slightly different in that it doesn't actually need to filter anything, and thus it only needs to manage the local isExpanded value, and just have the tasks and counter values come directly from the global state.

interface AllTodosState {
isExpanded: boolean;
}

@Component({
selector: 'all-tasks',
templateUrl: './all-tasks.component.html',
})
export class AllTasksComponent extends RxState<AllTodosState> {
readonly tasks$ = this.globalState.select('tasks');
readonly counter$ = this.globalState.select(
map((state) => state.tasks),
map((tasks) => tasks.length)
);
readonly isExpanded$ = this.select('isExpanded');

constructor(
@Inject(GLOBAL_RX_STATE) private globalState: RxState<GlobalState>
) {
super();
}
}