DEV Community

seif hassan
seif hassan

Posted on

Part Two: NGXS State Management: Beginner's Guide.

For part one..

How are the different concepts organised?

Now that you have gained familiarity with the various concepts of NGXS, let's explore how they interact with each other through a schema, which provides a clear visual representation of their relationships and interactions.

Image description
Schema explaining NGXS functioning


After we have set up NGXS, Now lets dive deep into our example:

add the following code in the imports of the @NgModule:

NgxsModule.forRoot([TodoState], {
developmentMode: !environment.production
}),

like this:

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    ...
    NgxsModule.forRoot([TodoState], {
      developmentMode: !environment.production
    }),
  ]
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

As we discussed earlier in the theoretical part, everything begins with a component. Therefore, let's proceed by writing the code for the app.component.ts file:

import { Component } from '@angular/core';
import {Select, Store} from '@ngxs/store';
import {AddTodo, EmptyTodo} from '../store/todo.actions';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  newTodo = '';

  constructor(private readonly store: Store) {}

  onAddTodo(): void {
    if (this.newTodo.length > 0) {
      this.store.dispatch(new AddTodo(this.newTodo));
    }
    this.newTodo = '';
  }

  onEmptyList(): void {
    this.store.dispatch(new EmptyTodo());
  }
Enter fullscreen mode Exit fullscreen mode

Let’s create the Actions in the todo.actions.ts:

export class AddTodo {
  static readonly type = '[Todo] Add';

  constructor(public newTodo: string) {
  }
}

export class EmptyTodo {
  static readonly type = '[Todo] Empty';
}
Enter fullscreen mode Exit fullscreen mode

Since the addTodo action requires the new todo as an input, we need to declare the constructor with newTodo as a parameter.

Following the triggering of these actions, certain code will be executed to modify the state. This is where the todo.state.ts file comes into play.

import {Action, State, StateContext} from '@ngxs/store';
import {AddTodo, EmptyTodo} from './todo.actions';

export interface TodoStateModel {
  todoList: string[];
}

@State<TodoStateModel>({
  name: 'todo',
  defaults: {
    todoList: [],
  }
})
export class TodoState {

  @Action(AddTodo)
  addTodo({patchState, getState}: StateContext<TodoStateModel>, {newTodo}: AddTodo): void {
    patchState({todoList: [...getState().todoList, newTodo]});
  }

  @Action(EmptyTodo)
  emptyTodo({patchState}: StateContext<TodoStateModel>): void {
    patchState({todoList: []});
  }
}
Enter fullscreen mode Exit fullscreen mode

The @State decorator allows us to define the initial state of the state container.

The functions preceded by the @Action annotation are associated with an action and will be executed when triggered.


Let's take a closer look at the more complex addTodo() function to understand its components:

First, it takes two parameters:

{patchState, getState}: StateContext<TodoStateModel}, which provides patchState() and getState() functions to interact with the state defined in the same file.
{newTodo}: AddTodo, which allows us to access data from the triggered action.


As mentioned earlier, the state is immutable. To modify the value of one or more state elements, NGXS provides the patchState() function. Here's an example of how it's used:

patchState({todoList: [...getState().todoList, newTodo]});

This function enables developers to specify parameters only for the fields they intend to modify, without needing to create a copy of the entire state.

To allow our component to access the todoList value from the state, we can add a getter function. Therefore, the todo.state.ts file is updated as follows:

import {Action, Selector, State, StateContext} from '@ngxs/store';
import {AddTodo, EmptyTodo} from './todo.actions';

export interface TodoStateModel {
  todoList: string[];
}

@State<TodoStateModel>({
  name: 'todo',
  defaults: {
    todoList: [],
  }
})
export class TodoState {

  @Selector()
  static getTodoList(state: TodoStateModel): string[] {
    return state.todoList;
  }

  @Action(AddTodo)
  addTodo({patchState, getState}: StateContext<TodoStateModel>, {newTodo}: AddTodo): void {
    patchState({todoList: [...getState().todoList, newTodo]});
  }

  @Action(EmptyTodo)
  emptyTodo({patchState}: StateContext<TodoStateModel>): void {
    patchState({todoList: []});
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can observe, we have added the getTodoList() function, which takes the state as a parameter and is annotated with @Selector().

Whenever the value of todoList changes, the code within the selector function is executed, and any component that listens to todoList through getTodoList() will automatically receive the updated value.


As mentioned earlier, this function acts as a decorator for the returned value. Therefore, if you decide to sort todoList in alphabetical order within the selector, each component listening to todoList through this function will receive the sorted list.

To complete the loop, we need to return to our App component to allow it to access the value of todoList from the state using the newly created selector. To achieve this, we add a new variable in our component preceded by the @select() annotation, with the selector function as the parameter.

Here is the final version of app.component.ts:

import { Component } from '@angular/core';
import {Select, Store} from '@ngxs/store';
import {AddTodo, EmptyTodo} from '../store/todo.actions';
import {TodoState} from '../store/todo.state';
import {Observable} from 'rxjs';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  @Select(TodoState.getTodoList) todoList$?: Observable<string[]>;

  newTodo = '';

  constructor(private readonly store: Store) {}

  onAddTodo(): void {
    if (this.newTodo.length > 0) {
      this.store.dispatch(new AddTodo(this.newTodo));
    }
    this.newTodo = '';
  }

  onEmptyList(): void {
    this.store.dispatch(new EmptyTodo());
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion:

🥳🙌👏Congratulations! You have successfully learned how to set up and utilize NGXS. Your next task, should you decide to accept it, is to adapt the example we have worked on to meet the requirements of your own project.

For further information on NGXS, you can refer to the official documentation, available HERE.

Well done, and best of luck with your NGXS implementation! 💫👋👋

Top comments (0)