The quickest way to learn Angular is to build something small from start to finish. On this page you build a reading list: you start with a component that shows three titles and end with an app that saves your books between visits. Each step adds one idea and links to the page that explains it in depth.
In the lab, the app on the left runs the code on the right. Read the code, play with the app, then move on to the next step.
Create the project
The Angular CLI creates the project, generates files and runs a development server that reloads the page every time you save. New projects in v22 use standalone components, signals and no Zone.js — exactly what this page uses.
npm install -g @angular/cli
ng new reading-list # standalone components, zoneless and signals by default
cd reading-list
ng generate component reading-list
ng serve # http://localhost:4200Build it in six steps
Every step is a real component. Change something in the live app, then look at the code to find which line made it happen.
A component is a class plus a template. The state is a signal holding three books, and @for draws one row per book.
My books
- Frankenstein
- Dracula
- Moby-Dick
import { Component, signal } from '@angular/core';
interface Book {
id: number;
title: string;
read: boolean;
}
@Component({
selector: 'app-reading-list',
template: `
<h3>My books</h3>
<ul>
<!-- track: how Angular tells the rows apart -->
@for (book of books(); track book.id) {
<li>{{ book.title }}</li>
}
</ul>
`,
})
export class ReadingList {
// the state: a signal holding a list
protected readonly books = signal<Book[]>([
{ id: 1, title: 'Frankenstein', read: true },
{ id: 2, title: 'Dracula', read: false },
{ id: 3, title: 'Moby-Dick', read: false },
]);
}What you used
| Step | Concepts | Read more |
|---|---|---|
| 1. Show a list | a component, signal(), @for with track | Signals · Control flow |
| 2. Add books | [value], (input), (keydown.enter), update() | Data binding |
| 3. Mark as read | computed(), @if, @empty | Signals · Control flow |
| 4. Split into components | input.required(), output() | input() and output() |
| 5. Move the state to a service | @Service(), inject(), asReadonly() | Services |
| 6. Save and filter | effect(), localStorage, a filter as computed | effect() · Lists · SSR-safe code |
Four bugs everyone writes once
These are the bugs that show up in almost every first Angular app. Turn each one on, see the symptom with your own eyes, then read why it happens.
- Frankenstein
- Dracula
- Moby-Dick
Try: press Add “Hamlet” with the bug on. The list shows four books, but the computed and the OnPush component still say three. Reset fixes everything at once.
Why: push() changes the array the signal already holds. The signal compares by reference, sees the same array and tells no one; the list only looks right because the click re-rendered this one component. Give the signal a new array: update((list) => [...list, book]).
// ✗ the same array, changed in place: the signal sees the same reference and notifies no one
this.books().push(book);
// ✓ a new array: the signal notifies everyone who reads it
this.books.update((list) => [...list, book]);All four have the same root: Angular trusts what you tell it. A signal trusts that a new value means a new reference, track trusts your key, and a computed trusts that it only calculates. Keep those promises and the framework does the rest.
Where to go next
- A page per book, with its own URL: routing.
- A real form with validation for adding books: Signal Forms.
- Books loaded from a server: HttpClient and resource().
- Tests for the store and the components: testing.
- Where the files go as the app grows: folder structure.