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
- add-todo
- 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.
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