Discover the power of @Ngrx/component-store to create a Local Component State
Tags: angular, state
Published: 2022-12-26 09:00:00

## Welcome to Angular challenges #5.
> _The aim of this series of Angular challenges is to increase your skills by practicing on real life exemples. Moreover you will have to submit your work though a PR which I or other can review; as you will do on real work project or if you want to contribute to Open Source Software._

The fifth challenge will teach you some good practices for architecture and creating scalable and readable components using Angular by refactoring a small Todo Application step by step. You will learn how to create reactive code with good separation of concerns and a great User and Developer eXperience. We will use and discover **@Ngrx/component-store** to create our local Component Store as well as **@tomalaforge/ngrx-callstate-store** to handle our loading and error states.

If you haven't done the challenge yet, I invite you to try it first by going to [Angular Challenges](https://github.com/tomalaforge/angular-challenges/blob/main/apps/crud/README.md) and coming back afterward to compare your solution with mine. (You can also submit a PR that I'll review)

---

Starting the challenge, we have the following code.

```typescript
@Component({
  standalone: true,
  imports: [CommonModule],
  selector: 'app-root',
  template: `
    <div *ngFor="let todo of todos">
      {{ todo.title }}
      <button (click)="update(todo)">Update</button>
    </div>
  `,
})
export class AppComponent implements OnInit {
  todos!: any[];

  constructor(private http: HttpClient) {}

  ngOnInit(): void {
    this.http
      .get<any[]>('https://jsonplaceholder.typicode.com/todos')
      .subscribe((todos) => {
        this.todos = todos;
      });
  }

  update(todo: any) {
    this.http
      .put<any>(
        `https://jsonplaceholder.typicode.com/todos/${todo.id}`,
        JSON.stringify({
          todo: todo.id,
          title: randText(),
          body: todo.body,
          userId: todo.userId,
        }),
        {
          headers: {
            'Content-type': 'application/json; charset=UTF-8',
          },
        }
      )
      .subscribe((todoUpdated: any) => {
        this.todos[todoUpdated.id - 1] = todoUpdated;
      });
  }
}
```

**Issues:** 
- Everything is located inside  a single component (http calls, state management, ui).
- No loading or error management. (not very user friendly)
- No Type checking (avoid the type any as possible)
- All properties are set declaratively.
- Void method are mutating some properties. Hard to debug as your component will grow.
- Manual subscribe without unsubscription

---

First, we need to move all HTTP calls to a **dedicated singleton Service**. This way, we can use this code everywhere by injecting it inside into our components.

```typescript
// singleton service provided inside the root injector. 
@Injectable({ providedIn: 'root' })
export class TodoService {
  private http = inject(HttpClient);

  getAllTodo = () =>
    this.http.get<Todo[]>(/*...*/);

  update = (id: number) =>
    this.http.put<Todo>(/*...*/);

  delete = (id: number) =>
    this.http.delete<void>(/*...*/);
}
```
Setting our service `providedIn: 'root'` create a unique instance and a tree-shakable service. If you want more detail, I invite you to read this article. 

---

The second action is to create a service dedicated to our component that will hold its state. This component is called a `Store` . In most application, we will have a global store. A global store holds the state for the entire application. However each component can also have a local state. that is not shared across the application. _(although it can be shared among its children)_

For this purpose, we will use [@Ngrx/component-store](https://www.npmjs.com/package/@ngrx/component-store)

> _You can use any other state management libraries like Akita, RxAngular, Elf, MiniRx Store, Subject as a Service … But I will focus on Component Store of NgRx._

Let's setup our store `app.store.ts` 

```typescript
@Injectable()
export class AppStore 
  extends ComponentStore<{/*State definition*/}> 
  implements OnStateInit, OnStoreInit {

  // selectors

  // updaters

  // effects

  ngrxOnStoreInit() {
    this.setState(/*initial state*/)
  }

  ngrxOnStateInit() {
    /*initialisation logic*/
  }
}
```
This is the minimal setup to create a CS. Let's go though it quickly:
- `OnStoreInit` and `OnStateInit` are two lifecycle hooks. In order to use them, we need to provide our store using  the `provideComponentStore` helper function. `OnStoreInit` is called immediately after the store is instantiated. `OnStateInit` is called after the state is created. Both hooks are only called once. 
- `selectors` are functions that extract a specific piece of state and are **shareable**. In RxJS, shareable observables can be called as many times as desired without being recomputed.
- `updaters` lets us create pure function to update the state. In comparison with `patchState` and `setState` , `updaters` have the current state as input. 
- `effects` handles side-effects like HTTP calls or any asynchronous task. 

If you want more details or dig deeper into the documentation, you can go [here](https://ngrx.io/guide/component-store).

**Important Note:** It's important to notice the `@Injectable` decorator. We didn't add `providedIn: 'root'` because we want our service to be tied to the lifecycle of the  component in which it will be provided.

If you want more details on how injectable services work in Angular, you should read [this article](https://dev.to/this-is-angular/mastering-injectable-services-a-comprehensive-guide-3kjg).

In order to tie our store to our component, we need to provide it at the component level:

```typescript
@Component({
  /*...*/
  providers: [provideComponentStore(AppStore)],
  /*...*/
})
```
**Remark:** As mentioned previously, we need to provide our store through `provideComponentStore` to use the `OnStateInit` and `OnStoreInit` lifecycle hooks inside our store. We will use these hooks to do some initialization logic.

Now, let's define our state. We will store a list of our Todos, a loading and en error indicators.

```typescript
interface AppState {
  todos: Todo[];
  loading: boolean;
  error?: string;
}
```
Then we need some selectors to read our state and we will create a View Model for our template.

> _A View Model is an object that combines all the properties needed in our template. This way, we only need to subscribe to one observable and pass one stream though our component._

```typescript
private readonly todos$ = this.select((state) => state.todos);
private readonly loading$ = this.select((state) => state.loading);
private readonly error$ = this.select((state) => state.error);

readonly vm$ = this.select(
  {
    todos: this.todos$,
    loading: this.loading$,
    error: this.error$,
  },
  { debounce: true }
);
```
**Remarks:** Since @Ngrx v15, the definition of our `viewmodel` has been simplified. This is similar to:

```typescript
readonly vm$ = this.select(
  this.todo$,
  this.loading$,
  this.error$,
  (todos, loading, error) => ({
    todos,
    loading,
    error,
  }),
  { debounce: true }
);
```
The next step is to create all the effects needed to fetch, update and delete our todo items.

### Fetch:

```typescript
// fetchTodo takes no input parameters
readonly fetchTodo = this.effect<void>(
  pipe(
    tap(() => this.patchState({ loading: true })),
    switchMap(() => this.todoService.getAllTodo().pipe(
        tapResponse(
           // success logic
          (todos) => this.patchState({ todos, loading: false }),
           // failure logic
          (error: Error) =>
            this.patchState({ error: error.message, loading: false })
        )
      )
    )
  )
);
```
We first set our loading indicator to true, then we call our HTTP service's `getAllTodo` method and we the `tapResponse` operator from NgRx to handle our response. This operator requires us to handle our error case and prevents us to killing the effect.

> _Don't forget to update the loading indicator in case of an error._

### Update:

```typescript
// updateTodo takes a todo id as parameter
readonly updateTodo = this.effect<number>(
  pipe(
    tap(() => this.patchState({ loading: true })),
    switchMap((id) => this.todoService.update(id).pipe(
        tapResponse(
          (todo) => this.updateTodos(todo),
          (error: Error) =>
            this.patchState({ error: error.message, loading: false })
        )
      )
    )
  )
);

// CS updater function to replace the new Todo item inside our Todo array
private readonly updateTodos = this.updater((state, todo: Todo) => ({
  error: undefined,
  loading: false,
  todos: state.todos.map((t) => (t.id === todo.id ? { ...todo } : t)),
}));
```
The `update` effect logic is similar to the `fetch` effect. The main difference is that the `update` effect has an input `id` of type `number`. So we set the generic type of our effect to `number` .

You can write your effect in the following way if you find it more readable:

```typescript
readonly updateTodo = this.effect((id$: Obsersable<number>) => 
  id$.pipe(
    // ... 
    )
  )
);
```
The `updater` function allows us to define a pure function that takes the current state as input and produces a new, immutable state as output.

### Delete: (similar to update)

```typescript
readonly deleteTodo = this.effect<number>(
  pipe(
    tap(() => this.patchState({ loading: true })),
    switchMap((id) => this.todoService.delete(id).pipe(
        tapResponse(
          () => this.deleteTodoState(id),
          (error: Error) =>
            this.patchState({ error: error.message, loading: false })
        )
      )
    )
  )
);

private readonly deleteTodoState = this.updater((state, todoId: number) => ({
  error: undefined,
  loading: false,
  todos: state.todos.filter((todo) => todo.id !== todoId),
}));

```
---

The last step is to build our component:

```typescript
@Component({
  standalone: true,
  imports: [NgIf, NgFor, MatProgressSpinnerModule, LetModule],
  providers: [provideComponentStore(AppStore)],
  selector: 'app-root',
  template: `
    <ng-container *nrxLet="vm$ as vm">
      <mat-spinner [diameter]="20" color="blue" *ngIf="vm.loading">
      </mat-spinner>
      <ng-container *ngIf="vm.error; else noError">
        Error has occured: {{ vm.error }}
      </ng-container>
      <ng-template #noError>
        <div *ngFor="let todo of vm.todos">
          {{ todo.title }}
          <button (click)="update(todo.id)">Update</button>
          <button (click)="delete(todo.id)">Delete</button>
        </div>
      </ng-template>
    </ng-container>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
  private appStore = inject(AppStore)

  // only one stream goes into our component
  vm$ = this.appStore.vm$;

  update(todoId: number) {
    this.appStore.updateTodo(todoId);
  }

  delete(todoId: number) {
    this.appStore.deleteTodo(todoId);
  }
}
```
In our template, we start by subscribing to our `viewmodel` using the `ngrxLet` directive provided by **@ngrx/component** package. We then handle our loading and error state and create our list of todos.

Our component is very simple and doesn't contain any logic. It just acts as a proxy, passing information to the corresponding service.

**Issues:**
- Inside the component store, having two separate properties to handle the loading and error state is error-prone as we need to update both in case of an error. 
- In our template, the loading and error indicator are global, which means they apply to all items. It would be nicer to have one indicator per item.

---

To resolve the first issue, we will use a small library called [ngrx-callstate-store](https://www.npmjs.com/package/@tomalaforge/ngrx-callstate-store). The library enhances the provided state to handle the **callstate** for us.

```typescript
@Injectable()
export class AppStore
  extends CallStateComponentStore<{todos: Todo[]}>
  implements OnStateInit, OnStoreInit
{/*...*/}
```
We can delete everything related to the loading or error state and our effects can be rewritten as follow _(Only the update effect is shown, but the fetch and the delete effects can be updated in a similar fashion)_

```typescript
readonly updateTodo = this.effect<number>(
  pipe(
    tap(() => this.startLoading()), // updater to patch our loading state
    switchMap((id) => this.todoService.update(id).pipe(
        tapResponse(
          // we could use stopLoading() 
          // but we would still need to update our todoList via an updater
          (todo) => this.updateTodos(todo),
          (error: unknown) => this.handleError( error ) // handle our UNKNOWN error
        )
      )
    )
  )
);

private readonly updateTodos = this.updater((state, todo: Todo) => ({
  ...state,
  callState: 'LOADED',
  todos: state.todos.map((t) => (t.id === todo.id ? { ...todo } : t)),
}));
```

---

The last issue is to split our component into smaller chunks. Is is always a good practice to isolate your data types into their own components and to have smaller component.

Let's create a `TodoItemComponent` :

```typescript
@Component({
  //...
  providers: [provideComponentStore(TodoItemStore)],
  template: `...`
})
export class TodoItemComponent {
  @Input() set todo(todo: Todo) {
    this.todoItemStore.patchState({ todo });
  }

  private todoItemStore = inject(TodoItemStore);

  vm$ = this.todoItemStore.vm$;

  update(todoId: number) {
    this.todoItemStore.updateTodo(todoId);
  }

  delete(todoId: number) {
    this.todoItemStore.deleteTodo(todoId);
  }
}
```
- We provide a store that is tied to this component. This means that each todo items will have its own instance of the store. This is necessary in order to create a loading or error state for each item. 
- We create a setter on the `@Input` property to patch our state. The entire logic is done inside our store. The component is only a proxy between the template and the store. 
- The same is true for `update` and `delete` methods, which we redirect to our store.

```typescript
@Injectable()
export class TodoItemStore extends CallStateComponentStore<{ todo: Todo }> {
  private todoService = inject(TodoService);
  private todosStore = inject(TodosStore);

  private readonly todo$ = this.select((state) => state.todo);

  readonly vm$ = this.select(
    {
      todo: this.todo$,
      loading: this.isLoading$,
      error: this.error$,
    },
    { debounce: true }
  );

  readonly updateTodo = this.effect<number>(
    pipe(
      tap(() => this.startLoading()),
      switchMap((id) => this.todoService.update(id).pipe(
          tapResponse(
            (todo) => {
              this.stopLoading();
              this.todosStore.updateTodo(todo);
            },
            (error: unknown) => this.handleError(error)
          )
        )
      )
    )
  );

  readonly deleteTodo = this.effect<number>(
    pipe(
      tap(() => this.startLoading()),
      switchMap((id) => this.todoService.delete(id).pipe(
          tapResponse(
            () => this.todosStore.deleteTodoState(id),
            (error: unknown) => this.handleError(error)
          )
        )
      )
    )
  );
}
```
- The parent store `TodoStore` is injected inside `TodoItemStore` . The parent store will have the same instance among all its children. We need to get the store instance to update the Todo list. `TodoItemStore` will mostly handle the loading and error state. 
- The `updateTodo` and `deleteTodo` methods set the current callState to `LOADING` , then make a HTTP request and update the parent state on success or update the current callstate with an error otherwise. 

---

That's it; we have build a nice todo app. We could add a small form to add new todos to the list, but that is beyond the scope of this challenge. You can find the final code in the form of a Pull Request [here](https://github.com/tomalaforge/angular-challenges/pull/27). _(If you want to get it up and running, you can clone the project, check out the solution branch and `run nx serve crud`)_. 

---

I hope you enjoyed this fifth challenge and learned some new Angular techniques.

If you found this article useful, please consider supporting my work by giving it some likes ❤️❤️ to help it reach a wider audience. Don't forget to share it with your teammates who might also find it useful. Your support would be greatly appreciated.

👉 Other challenges are waiting for you at [Angular challenges](https://github.com/tomalaforge/angular-challenges). Come and try them. I'll be happy to review you!

Follow me on [Twitter](https://twitter.com/laforge_toma) or [Github](https://github.com/tomalaforge) to read more about upcoming Challenges! Don't hesitate to ping me if you have more questions.