- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
Build Reactive Applications with Signals in Angular
This lab will guide you through building a small Globomantics product dashboard with Angular's modern signals API. Learn how to manage local component state, derive new values automatically from existing ones, react to inputs from a parent component and load data over HTTP without ever writing a manual subscription. This lab walks through the four pillars of signal-based reactivity in Angular, where you will turn a static product card into a fully reactive UI driven by signal, computed, linkedSignal and httpResource. In this lab you will replace hard-coded values with writable signals, derive a running total that updates automatically when its inputs change and convert a decorator-based @Input() into a signal input that the rest of the component reacts to. You will also learn how to load a product catalog with httpResource and render its loading, error and value states using Angular's built-in control flow.
Lab Info
Table of Contents
-
Challenge
Introduction
In this Guided Code Lab, you will build the Globomantics Product Catalog Dashboard, a small Angular single-page app that demonstrates each of Angular's signal primitives — writable, computed, linked, and
httpResource.Each task can be validated individually by clicking the Validate button next to it.
If you get stuck, every task has a Task Solution section you can expand to reveal the answer. This can be found under the FEEDBACK/CHECKS section of every Task.
info> The
solutions/folder at the top of the workspace contains the final state of every task.A failed task will list one or more failed checks under its Checks section, each with a specific message describing what went wrong.
The starting point of the lab is an Angular workspace named
catalog. The current directory of the built-in Terminal is set to that workspace. The project already contains:- A header, navigation, and footer shell branded for Globomantics.
- A
ProductPickerparent component with two sample products you can switch between. - A
ProductListchild component whose template already has the quantity controls (a−button, a quantity value<span>, and a+button), as well as labelled rows for the unit price and total. Your tasks add the signal logic behind these elements. - A
Catalogcomponent with empty loading, error, and list containers, ready for you to wire up to an HTTP-backed signal in the final step. - A pre-configured mock backend (
mock-api.js) that servesGET /api/productson port3001, plus aproxy.conf.jsonso requests to/api/*from the Angular dev server resolve locally.
You can start everything up by running
ng servein the Terminal or by clicking the Run button. This launches the Angular dev server in parallel. Once it builds, open the running app at:{{localhost:4200}}
The first time the lab starts, dependencies are already installed for you. Products are provided from the simulated database served by the mock-api server.
Why signals?
Signals are Angular's modern reactive primitive. A signal is a value that other code can read and react to. When the signal's value changes, every place that reads it, re-evaluates automatically. There are no manual subscriptions to manage, no
BehaviorSubjectboilerplate and no need to trigger change detection by hand. The same()call works for the simplest writable counter and for an HTTP response that hasn't loaded yet.Across the next four steps, you will replace plain properties and traditional patterns with the four signal primitives, one at a time, until the whole dashboard is reactive end to end.
Click the button with the arrow pointing to the right (>) to get started with the next step.
-
Challenge
Getting Started with Signals
A writable signal wraps a single value and lets Angular track everyone who reads it. You can create one with
signal(initial), read it by calling it and update it with.set(value)or.update(fn).import { signal } from '@angular/core'; count = signal(0); // read const current = this.count(); // 0 // write this.count.set(5); this.count.update((v) => v + 1);The function-call read (
count()) is what registers the reader as a dependency. There is no.valueproperty and nosubscribe(). Whenever the signal changes, every reader (template bindings, computeds, effects) is notified automatically. Event handlers in templates can call a writable signal's updaters directly. Use.update(fn)when the new value depends on the current one, and.set(value)when it does not.<button (click)="count.update((v) => v + 1)">+</button> <button (click)="count.set(0)">Reset</button> ``` You replaced a static value with a writable signal, bound it to the template with the function-call read form, and updated it from three buttons using both `.update()` and `.set()`. Angular re-rendered the bound span on every change with no `subscribe()`, no `ChangeDetectorRef` and no `BehaviorSubject`. -
Challenge
Deriving State with Computed Signals
When a value is derived from one or more other signals, Angular's
computed()expresses that derivation directly. A computed signal is read-only and pure: you give it a function, Angular runs that function only when one of the signals it reads has changed.import { computed, signal } from '@angular/core'; firstName = signal('Ada'); lastName = signal('Lovelace'); fullName = computed(() => `${this.firstName()} ${this.lastName()}`);You read a computed the same way you read any signal, call it:
fullName(). Reading dependencies via the call form (this.firstName()) inside the callback is what registers them, so Angular knows exactly which signals affect which derived values. A computed signal lives on the class as a field, just like a writable one. Its callback returns the derived value and reads any source signals using the function-call form:import { computed, signal } from '@angular/core'; celsius = signal(20); fahrenheit = computed(() => this.celsius() * 9 / 5 + 32);Reading the source signal via the function-call form inside the callback is what registers the computed as a dependant. Whenever the source changes, the computed re-evaluates and only then. You added a second writable signal and tied the two together with a computed signal. Whenever
quantityorunitPricechanges, the rendered total recomputes automatically. -
Challenge
Adapting External State with `linkedSignal`
Up to this point the values managed by
signal()have all been local to the component. Component inputs — the values a parent passes in via property binding — used to be expressed with the@Input()decorator. Angular's modern signal-based inputs let a component expose an input as a signal instead — a value you read with the function-call form and that integrates naturally withcomputed,effect, and the next primitive (linkedSignal).import { Component, input } from '@angular/core'; @Component({ /* … */ }) export class Greeting { username = input.required<string>(); greeting = computed(() => `Hello, ${this.username()}!`); }input.required<T>()declares a required input. Angular guarantees a value is provided before the component reads it. Inside the class, you read the input by calling it (this.username()). The parent's binding syntax does not change:<app-greeting [username]="..." />keeps working exactly as before. A plainsignal()is purely local. Acomputed()is purely derived (and read-only).linkedSignalis the bridge: a writable signal whose value is re-seeded from a source signal whenever that source changes, but which the user can edit between source changes.import { linkedSignal, signal } from '@angular/core'; city = signal('Warsaw'); draftCityName = linkedSignal({ source: this.city, computation: (city) => city, });When
citychanges,draftCityNameis reset to the new city, discarding any in-flight edit. Between changes,draftCityName.set(...)and.update(...)work just like on a plain writable signal. Thecomputationcallback receives the latest source value and returns whatever should seed the linked signal. It can be the source itself, a property of it, or any pure transformation. You replaced a decorator-based@Input()with a signal input, then turnedquantityinto alinkedSignalthat re-seeds whenever the selected product changes. The component is now fully signal-based and tracks an external input without losing local edits between switches. -
Challenge
Fetching Reactive Data with `httpResource`
The
httpResourceturns an HTTP request into a resource — a small bundle of three signals:value()— the parsed response body, orundefineduntil the first response landsisLoading()—truewhile a request is in flighterror()— any request error
Because all three are signals, they integrate naturally with templates (no
asyncpipe), withcomputed, and witheffect. The first prerequisite is that Angular's HTTP client has been registered with the application. AnhttpResourceis declared like any other class field. The argument is a URL factory, a function returning the URL the resource should request. Because the factory runs in a reactive context, any signals it reads become dependencies; when they change, the resource refetches automatically.import { httpResource } from '@angular/common/http'; // Static URL — fetched once, never refetched. weather = httpResource<Weather>(() => '/api/weather'); // Reactive URL — refetched whenever userId() changes. profile = httpResource<UserProfile>(() => `/api/users/${this.userId()}`);For a static URL the factory simply returns a string. The generic parameter (
<T>) typesvalue()for you, so consumers get full IntelliSense on the response body. Angular's built-in control flow blocks —@if,@else if,@else,@for— read signals directly. There is noasyncpipe and no*ngIf/*ngForstructural directive. The pattern for any resource is the same: branch on its three signals.@if (resource.isLoading()) { <p>Loading…</p> } @else if (resource.error()) { <p>Something went wrong.</p> } @else { @for (item of resource.value() ?? []; track item.id) { <li>{{ item.label }}</li> } }Two things are worth highlighting:
- The
?? []guard handles the brief moment before the first response arrives, when the resource's value is stillundefined. - The
trackexpression on@forlets Angular reuse DOM nodes efficiently when the underlying collection changes — pick a stable identifier from each item.httpResourceexposes more than read-only signals. The object it returns also has areload()method that re-issues the request and flipsisLoading()back totruewhile the response is in flight. Because the template already readsisLoading(),error(), andvalue(), callingreload()is enough to swing the UI back through its loading state and then render the fresh response. No new subscription, no new flag, no manual change detection.
// Anywhere with access to the resource: this.weather.reload();A common UI pairing is a Refresh button bound to
(click)="resource.reload()"and[disabled]="resource.isLoading()", so the user cannot kick off a second request while the first is still loading. Congratulations! You have built a fully signal-based Angular UI:- Step 2 introduced
signalandupdate— local writable state on a class. - Step 3 added
computed— derived state that recalculates only when its dependencies change. - Step 4 swapped a decorator-based
@Input()forinput.required<T>(), then turnedquantityinto alinkedSignalthat re-seeds from a source while staying writable. - Step 5 loaded data through
httpResource, exposingvalue,isLoading, anderroras signals consumed directly by built-in control flow, then wired a Refresh button that callsreload()to re-issue the request on demand.
You never wrote a
subscribe(), never reached for theasyncpipe, never calledmarkForCheck(). Reactivity now flows automatically from the producers (signals, inputs, resources) to the consumers (computeds, templates) through the dependency graph Angular tracks for you.Where to take this next
The same primitives compose into much richer patterns. A search field bound to a
signal<string>and read inside anhttpResourceURL factory refetches on every keystroke. A category dropdown bound to asignal<Category>triggers a refetch when the user picks a new value. Aneffectthat callsreload()on an interval keeps a dashboard live without any manual subscription bookkeeping. Each of these is a few lines on top of what you already have. Every piece still flows through the same signal graph.
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.