Featured resource
2026 Tech Forecast
2026 Tech Forecast

1,500+ tech insiders, business leaders, and Pluralsight Authors share their predictions on what’s shifting fastest and how to stay ahead.

Download the forecast
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

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 platform
Lab Info
Level
Intermediate
Last updated
May 12, 2026
Duration
45m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. 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 ProductPicker parent component with two sample products you can switch between.
    • A ProductList child 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 Catalog component 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 serves GET /api/products on port 3001, plus a proxy.conf.json so requests to /api/* from the Angular dev server resolve locally.

    You can start everything up by running ng serve in 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 BehaviorSubject boilerplate 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.

  2. 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 .value property and no subscribe(). 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`.
  3. 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 quantity or unitPrice changes, the rendered total recomputes automatically.

  4. 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 with computed, 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 plain signal() is purely local. A computed() is purely derived (and read-only). linkedSignal is 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 city changes, draftCityName is reset to the new city, discarding any in-flight edit. Between changes, draftCityName.set(...) and .update(...) work just like on a plain writable signal. The computation callback 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 turned quantity into a linkedSignal that 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.

  5. Challenge

    Fetching Reactive Data with `httpResource`

    The httpResource turns an HTTP request into a resource — a small bundle of three signals:

    • value() — the parsed response body, or undefined until the first response lands
    • isLoading()true while a request is in flight
    • error() — any request error

    Because all three are signals, they integrate naturally with templates (no async pipe), with computed, and with effect. The first prerequisite is that Angular's HTTP client has been registered with the application. An httpResource is 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>) types value() 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 no async pipe and no *ngIf / *ngFor structural 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 still undefined.
    • The track expression on @for lets Angular reuse DOM nodes efficiently when the underlying collection changes — pick a stable identifier from each item. httpResource exposes more than read-only signals. The object it returns also has a reload() method that re-issues the request and flips isLoading() back to true while the response is in flight. Because the template already reads isLoading(), error(), and value(), calling reload() 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 signal and update — 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() for input.required<T>(), then turned quantity into a linkedSignal that re-seeds from a source while staying writable.
    • Step 5 loaded data through httpResource, exposing value, isLoading, and error as signals consumed directly by built-in control flow, then wired a Refresh button that calls reload() to re-issue the request on demand.

    You never wrote a subscribe(), never reached for the async pipe, never called markForCheck(). 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 an httpResource URL factory refetches on every keystroke. A category dropdown bound to a signal<Category> triggers a refetch when the user picks a new value. An effect that calls reload() 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

Mateo is currently a full stack web developer working for a company that has clients from Europe and North America. His niche in programming was mostly web oriented, while freelancing, working on small startups and companies that require his services. Go(lang), Elixir, Ruby and C are his favorite languages and also the ones he’s mostly working with other then PHP in day to day work. He has a big passion for learning and teaching what he knows the best. His big interests recently have been the fields of DevOps, Linux, functional programming and machine learning.

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.

Get started with Pluralsight