Skip to content

Storing State in an Angular Service

In the last lesson, we set up a form component that would emit an event that includes the data that the user entered into the form. We are using that form component in our HomeComponent now, but all we are doing is logging out the value from the form.

Now we actually need to do something with it. This is going to be our first experience with dealing with state in the application. We have an entire module dedicated to state coming up soon. Of course, we are going to discuss what state is and what it looks like in detail in that module, but it is useful to have a basic idea now.

A Quick Intro to State

Let’s use a real analogy. Consider a hammer, a hammer does not have state (we probably could consider a hammer to have state, but let’s keep things simple). Consider a light bulb, a light bulb does have state — it can be in the on state or the off state.

When we first load our application it is in its default state. At the moment, that is the only state our application can be in because there isn’t anything that modifies our application’s state yet (without getting too technical). A user enters some data into a form, submits it, but nothing happens. However, we do want something to happen.

We want to add a feature so that the data the user enters is remembered and displayed in the form of a todo in a list. When we do this, the application’s state will have changed because now we have some data in memory. The application has changed in some way.

When thinking of state, think of a lightbulb, or a traffic light, or a computer. In what ways can our application be interacted with (internally or externally) to change the way it behaves/what it stores in memory? Generally, the state of an Angular application refers to the data that is being stored in memory (i.e. the stuff that will be lost when the application is refreshed).

Storing State in a Service

We have some todo data that we are handling, and we want that as part of our application’s state somehow. Specifically, we want to store the data in memory and use that data to display a list of all the todos that have been added.

This is where we will reach for a service (i.e. an @Injectable() class). If you remember from the basics section, a service can be shared with our entire application. It’s a great way to share data between components, and store data that we want to persist throughout the life of the application.

This service is going to have a publicly exposed signal. We can then use that signal from anywhere in the application, and can react any time it is updated with a new value.

This signal will only retain its value for as long as the app runs. As soon as we refresh the application our service is going to be destroyed along with any data in memory. If we want to persist data in memory, we will need to use some kind of external storage mechanism and then re-initialise our state upon loading the application. This is a challenge we will tackle in our “real” application walkthrough later.

import { Injectable, signal } from '@angular/core';
import { Todo } from '../interfaces/todo';
@Injectable({
providedIn: 'root',
})
export class TodoService {
// We only want this class to be able to
// update the signal (# makes it private)
#todos = signal<Todo[]>([]);
// This can be read publicly
todos = this.#todos.asReadonly();
addTodo(todo: Todo) {
this.#todos.update((todos) => [...todos, todo]);
}
}

The #todos signal is what will be responsible for storing the state in our application for now. Note that we have specifically named it #todos with a # prefix. This is a special syntax that can be used to create a “private field” — it is like using the private keyword which means that only code in this class can access it. The goal here is to have a private signal that can only be updated from within this class, and then a signal that is derived from that private signal that is exposed publicly and is read only. This helps keep things safer and more organised — we know that the only way this signal’s value can be updated is from within this file.

We have an addTodo method that will take in the todo we want to add. It will use the spread syntax to create a new array containing all of the old todos (if there are any) and our new todo. It will then update our private signal with that value, which will also update our publicly exposed read only signal.

To utilise this service, we can inject it into our HomeComponent and pass the todo data from our form to the addTodo method in the service.

import { Component, inject } from '@angular/core';
import { TodoFormComponent } from './ui/todo-form.component';
import { TodoService } from '../shared/data-access/todo.service';
@Component({
selector: 'app-home',
template: `
<h1>Todo</h1>
<app-todo-form (todoSubmitted)="todoService.addTodo($event)" />
`,
imports: [TodoFormComponent],
})
export default class HomeComponent {
todoService = inject(TodoService);
}

Notice that we are calling the addTodo method of the TodoService directly in the event binding for the todoSubmitted event (we have deleted the previous method we were using to log out the value).

Displaying data from a service

We now have a way to create a todo and have that data stored in the application. Now all we need to do is make use of that data.

import { Component, input } from '@angular/core';
import { Todo } from '../../shared/interfaces/todo';
@Component({
selector: 'app-todo-list',
template: `
<ul>
@for (todo of todos(); track $index){
<li>
<a>{{ todo.title }}</a>
</li>
} @empty {
<li>Nothing to do!</li>
}
</ul>
`,
})
export class TodoListComponent {
todos = input.required<Todo[]>();
}

We could have just added this list directly to our HomeComponent, but again, we are trying to keep things organised. Instead of the HomeCompnent having the responsibility of displaying the list, we create a separate component to handle it.

This component takes an input of todos and will loop over them in the template displaying a list item for each one. Our todos don’t have any unique properties to track by at the moment, so we just use the default $index option to track them by their index. In the next lesson on routing we will need to add a unique id for our todos and we will come back and update this.

We are also utilising the @empty syntax here to display a message if there are no todos.

Now we can use this component in our HomeComponent to display the list. Try doing that on your own before continuing.

import { Component, inject } from '@angular/core';
import { TodoFormComponent } from './ui/todo-form.component';
import { TodoService } from '../shared/data-access/todo.service';
import { TodoListComponent } from './ui/todo-list.component';
@Component({
selector: 'app-home',
template: `
<h1>Todo</h1>
<app-todo-form (todoSubmitted)="todoService.addTodo($event)" />
<app-todo-list [todos]="todoService.todos()" />
`,
imports: [TodoFormComponent, TodoListComponent],
})
export default class HomeComponent {
todoService = inject(TodoService);
}

Now, every time we add a todo it will instantly pop up in our list. Pretty cool right?

I mean, it’s terribly ugly right now… but it works!

In the next lesson, we are going to handle adding routing to a detail page to view the description of the todo. Going to an entire detail page for such a small amount of data is probably overkill here, but I mostly just want to demonstrate the concepts.