2

I'm trying to learn reactive state for Angular using NgRx by creating a Todo list application.

To do this, first I started by creating two main components

  1. add-todo
  2. todo-list

Then I created an interface for what a todo should be.

export interface Todo {
    id: string;
    content: string;
}

Then I started working on the state management. First I created actions. I am only focused on 2 actions for now, create todo and delete todo.

import { createAction, props } from "@ngrx/store";

export const createTodo = createAction(
    '[Add-Todo component] Add todo',
    props<{ content: string }>()
)

export const deleteTodo = createAction(
    '[Todo-List component] Delete todo',
    props<{ id: string }>()
)

After that I started working on the reducers. I decided to just do the create todo reducer so I could get a list showing up in my app before worrying about deletion.

import { Action, on, createReducer } from "@ngrx/store";
import { createTodo, deleteTodo } from "./todo.actions";
import { Todo } from "src/app/models/todo.model";

export interface todoState {
    todos : Todo[]; 
}

export const initialState: todoState = {
    todos: []
}

export const todoReducer = createReducer(
    initialState,
    on(createTodo, (todoState, {content}) => ({
        todos: [...todoState.todos, {id: Date.now().toString(), content: content}]
    }))
)

At this point, I wanted to verify that I could add todos to the NgRx store. My add-todo component file looks like this.

import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs'
import { Todo } from '../models/todo.model';
import { createTodo, deleteTodo } from '../state/todo/todo.actions';

@Component({
  selector: 'app-add-todo',
  templateUrl: './add-todo.component.html',
  styleUrls: ['./add-todo.component.css']
})
export class AddTodoComponent {
  allTodos$ : Observable<Todo[]>;
  todo:string = '';

  constructor(private store: Store<{allTodos: Todo[]}>){
    this.allTodos$ = store.select('allTodos');
  }
  addTodo(){
    this.store.dispatch(createTodo({content:this.todo}));
    this.todo = '';
  }
}

And I can verify this works by using the angular devtools extension of the chrome browser. Here's a screenshot that I took after adding 2 todos. NgRx store storing data

So far so good. Now I want to subscribe to that value and update my todo-list component any time a new todo is added. But this is where I'm running into a problem.

First, I'm creating an app.state.ts file. I figure this is good practice as I can add all different parts of state to this file and have each component import a specific part of it. That file looks like this.

import { todoState } from "./todo/todo.reducers";

export interface AppState {
    todos: todoState;
}

Then I'm creating my todo.selector.ts file

import { createSelector } from "@ngrx/store";
import { AppState } from "../app.state";
import { todoState } from "./todo.reducers";

export const selectTodos = (state: AppState) => state.todos;
export const SelectAllTodos = createSelector(
    selectTodos,
    (state: todoState) => state.todos
)

Next I'm registering my reducer inside my app.module.ts file

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { StoreModule } from '@ngrx/store';
import { TodoListComponent } from './todo-list/todo-list.component';
import { AddTodoComponent } from './add-todo/add-todo.component';

import { todoReducer } from './state/todo/todo.reducers';

import { FormsModule } from '@angular/forms';

@NgModule({
  declarations: [
    AppComponent,
    TodoListComponent,
    AddTodoComponent
  ],
  imports: [
    BrowserModule,
    StoreModule.forRoot({todos: todoReducer}, {}),
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

And finally I'm creating an allTodos$ property in my todo-list component then I can use to update my unordered list in the template. This is where my terminal is telling me the error is.

import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { SelectAllTodos } from '../state/todo/todo.selector';


@Component({
  selector: 'app-todo-list',
  templateUrl: './todo-list.component.html',
  styleUrls: ['./todo-list.component.css']
})
export class TodoListComponent {
  public allTodos$ = this.store.select(SelectAllTodos);

  constructor(private store: Store){
  
  }
}

Specifically, this line.

public allTodos$ = this.store.select(SelectAllTodos);

The full error I'm getting is this:

Error: src/app/todo-list/todo-list.component.ts:12:40 - error TS2769: No overload matches this call. Overload 1 of 9, '(mapFn: (state: object) => Todo[]): Observable<Todo[]>', gave the following error. Argument of type 'MemoizedSelector<AppState, Todo[], (s1: todoState) => Todo[]>' is not assignable to parameter of type '(state: object) => Todo[]'. Types of parameters 'state' and 'state' are incompatible. Property 'todos' is missing in type '{}' but required in type 'AppState'. Overload 2 of 9, '(key: never): Observable', gave the following error. Argument of type 'MemoizedSelector<AppState, Todo[], (s1: todoState) => Todo[]>' is not assignable to parameter of type 'never'.

12 public allTodos$ = this.store.select(SelectAllTodos); ~~~~~~~~~~~~~~

src/app/state/app.state.ts:4:5 4 todos: todoState; ~~~~~ 'todos' is declared here.

Any help would greatly be appreciated. This error has me scratching my head.

Update I've created a stackblitz for easier debugging. StackBlitz Link

onTheInternet
  • 6,421
  • 10
  • 41
  • 74

1 Answers1

1

Could you try to initialize your variable inside ngOnInit?

export class TodoListComponent implement OnInit {
  public allTodos$:Observable<Todo[]>

  constructor(private store: Store){
  }

  ngOnInit(){
    this.allTodos$ = this.store.select(SelectAllTodos);
  }
}

EDIT, There is something weird with your AppState.

You define it as:

export interface AppState {
    todos: todoState;
}

which is the equivalent as:

export interface AppState {
    todos: {
       todos:Todo[]
   }
}

So at the end, I imagine that you should replace:

export const SelectAllTodos = createSelector(
    selectTodos,
    (state: todoState) => state.todos
)

by

export const selectTodos = (state: todoState) => state.todos;
export const SelectAllTodos = createSelector(
    selectTodos,
    (state: Todo[]) => state
)

EDIT 2: If you change this code, it would be fine

  constructor(private store: Store<{ todos: Todo[] }>) {
    this.allTodos$ = store.select('todos');
  }
Wandrille
  • 6,267
  • 3
  • 20
  • 43