Table of contents
- Step 1: Install NGRX
- Step 2: Define the Model for ToDo
- Step 3: Define the Service and Actions
- Step 4: Define the Reducers
- Step 5: Define the Effects
- Step 6: Define the interface for the Store
- Step 7: Register the Store and Effects in the main standalone component
- Step 8: Use the Store in ToDo List Component
- Step 9: Register the ToDo List Component in the main component
- Conclusion
NGRX is a popular library for state management in Angular applications. It helps to manage the state of an application in a predictable and scalable way. In this guide, we will go through the step-by-step process of implementing NGRX by building a small Todo application.
You can refer to the full code here.
Step 1: Install NGRX
To install NGRX, run the following command in your terminal:
npm install @ngrx/store @ngrx/effects --save
The @ngrx/store
package provides the core state management functionality for NGRX. The @ngrx/effects
package provides a way to handle side-effects in your application.
Our application folder structure will look like this -
src/
| store/
| actions.ts
| reducers.ts
| selectors.ts
| store.ts
| todo.model.ts
| main.ts
| todo.component.ts
| todo.component.html
Step 2: Define the Model for ToDo
The first step is to define the model of your data in store/todo.model.ts
-
export interface Todo {
id: number | string;
description: string;
completed: boolean;
}
In this example, we define the Todo
interface with some properties.
Step 3: Define the Service and Actions
Service will contain our todo API service with the getAll
function. This will act as a fake backend service. We will be going to use the Observable and imitate the latency to show the loader. Define the todo service in store/service.ts
@Injectable()
export class ToDoService {
// fake backend
getAll(): Observable<Todo[]> {
return of(
[{
id: 1,
description: 'description 1',
completed: false
},
{
id: 2,
description: 'description 2',
completed: false
}]
).pipe(delay(2000))
}
}
Actions are messages that describe a state change in your application. They are plain JavaScript objects that have a type property and an optional payload. Define the actions in store/actions.ts
export const loadTodos = createAction('[Todo] Load Todos');
export const loadTodosSuccess = createAction('[Todo] Load Todos Success', props<{ todos: Todo[] }>());
export const loadTodosFailure = createAction('[Todo] Load Todos Failure', props<{ error: string }>());
export const addTodo = createAction('[Todo] Add Todo', props<{ todo: Todo }>());
export const updateTodo = createAction('[Todo] Update Todo', props<{ todo: Todo }>());
export const deleteTodo = createAction('[Todo] Delete Todo', props<{ id: string }>());
In this example, we define several actions for managing the Todo
state. The loadTodos
action is used to load the list of todos from the server. The loadTodosSuccess
action is dispatched when the todos are loaded successfully. The loadTodosFailure
action is dispatched when there is an error loading the todos. The addTodo
, updateTodo
, and deleteTodo
actions are used to add, update, and delete todos respectively.
Step 4: Define the Reducers
Reducers are pure functions that take the current state and an action and return a new state. They are responsible for handling the state changes in your application. Define the actions in store/reducers.ts
export interface TodoState {
todos: Todo[];
loading: boolean;
error: string;
}
export const initialState: TodoState = {
todos: [],
loading: false,
error: ''
};
export const todoReducer = createReducer(
initialState,
on(TodoActions.loadTodos, state => ({ ...state, loading: true })),
on(TodoActions.loadTodosSuccess, (state, { todos }) =>({ ...state, todos, loading: false })),
on(TodoActions.loadTodosFailure, (state, { error }) => ({ ...state, error, loading: false })),
on(TodoActions.addTodo, (state, { todo }) => ({ ...state, todos: [...state.todos, todo] })),
on(TodoActions.updateTodo, (state, { todo }) => ({ ...state, todos: state.todos.map(t => t.id === todo.id ? todo : t) })),
on(TodoActions.deleteTodo, (state, { id }) => ({ ...state, todos: state.todos.filter(t => t.id !== id) })),
);
In this example, we define a reducer for managing the Todo
state. The todoReducer
function takes the initialState
and a set of reducer functions defined using the on
function from @ngrx/store
. Each reducer function handles a specific action and returns a new state.
Step 5: Define the Effects
Effects are services that listen for actions and perform side-effects such as HTTP requests or interacting with the browser's APIs.
@Injectable()
export class TodoEffects {
loadTodos$ = createEffect(() =>
this.actions$.pipe(
ofType(TodoActions.loadTodos),
mergeMap(() =>
this.todoService.getAll().pipe(
map((todos) => TodoActions.loadTodosSuccess({ todos })),
catchError((error) =>
of(TodoActions.loadTodosFailure({ error: error.message }))
)
)
)
)
);
constructor(private actions$: Actions, private todoService: ToDoService) {}
}
In this example, we define an effect for handling the loadTodos
action. The loadTodos$
effect listens for the loadTodos
action using the ofType
operator from @ngrx/effects
. When the action is dispatched, the effect calls the getAll
method from the TodoService
dispatches either the loadTodosSuccess
or loadTodosFailure
action depends on the result.
Step 6: Define the interface for the Store
This will be defined in the store/sotre.ts
file -
export interface AppState {
todo: TodoState
}
export interface AppStore {
todo: ActionReducer<TodoState, Action>;
}
export const appStore: AppStore = {
todo: todoReducer
}
export const appEffects = [TodoEffects];
AppState
interface will define all the feature properties of the application. Here we have a single feature called as todo
which is of type TodoState
. We can have multiple features inside our application like this.
AppStore
interface will define all the reducer types used in our app. In this case, we have a single todo reducer so we will map the todoReducer
to the todo
property. appStore
will be used to config our Store Module.
appEffects
will have an array of defined effects classes. This will be used to register the effects in the application.
Step 7: Register the Store and Effects in the main standalone component
To use the NGRX store in your application, you need to register the StoreModule
in your AppModule
. We will be using standalone components here -
@Component({
selector: 'my-app',
standalone: true,
imports: [
CommonModule
],
template: ``,
})
export class App {
}
bootstrapApplication(App, {
// register the store providers here
providers: [
provideStore(appStore),
provideEffects(appEffects),
ToDoService
]
});
In this example, we register the store using the appStore
. We also register the Effects with appEffects
.
Step 8: Use the Store in ToDo List Component
To use the store in your components, you need to inject the Store
service and dispatch actions. We will create a standalone TodoListComponent
in src/todo-list.component.ts
with the html file in src/todo-list.component.html
@Component({
standalone: true,
selector: 'app-todo-list',
imports: [NgFor, NgIf, AsyncPipe, JsonPipe],
templateUrl: './todo-list.component.html'
})
export class TodoListComponent {
todos$: Observable<Todo[]>;
isLoading$: Observable<boolean>;
constructor(private store: Store<AppState>) {
this.todos$ = this.store.select(todoSelector);
this.isLoading$ = this.store.select(state => state.todo.loading);
this.loadTodos();
}
loadTodos() {
this.store.dispatch(TodoActions.loadTodos());
}
addTodo(index: number) {
const todo: Todo = {id: index, description: 'New Todo', completed: false };
this.store.dispatch(TodoActions.addTodo({ todo }));
}
complete(todo: Todo) {
this.store.dispatch(TodoActions.updateTodo({todo : {...todo, completed: true}}));
}
}
In this example, we define a component that uses the Store
service to load and display todos. We inject the Store
service and select the todos
property from the state using the select
method. We also define three methods for dispatching the loadTodos
, addTodo
and updateTodo
actions. The async
pipe is used to subscribe to the todos$
observable and display the list of todos.
Step 9: Register the ToDo List Component in the main component
@Component({
selector: 'my-app',
standalone: true,
imports: [
CommonModule,
TodoListComponent
],
template: `
<app-todo-list/>
`,
})
export class App {
}
Add TodoListComponent
in the imports array. Add the <app-todo-list/>
tag to display the TodoListComponent.
Conclusion
In this guide, we have gone through the step-by-step process of implementing NGRX in an Angular application. We have seen how to define the state, actions, reducers, effects, and how to use the store in components. By following these steps, you can easily manage the state of your application in a scalable and predictable way using NGRX.