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

Signal-Powered Angular Storefront

You are building a lightweight Angular storefront for a fitness-gear shop called 'Carved Rock Fitness'. The storefront lets customers browse a catalog of products fetched from an API, add items to a cart, select a shipping method that resets whenever the cart changes, and see a live running total. Each feature maps directly to a signals concept: writable signals for cart contents, computed() for the order total, linkedSignal() for the resettable shipping choice, httpResource() for catalog data, and a shared CartService that keeps signal state accessible across multiple components.

Lab platform
Lab Info
Level
Beginner
Last updated
Oct 22, 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

    Step 1: Tour the Carved Rock Fitness starter

    Carved Rock Fitness: a signals-driven storefront

    You are joining the team building Carved Rock Fitness, a fitness-gear storefront. The catalog, cart, shipping picker, and checkout summary all exist as component shells already, but none of them hold real state yet - every one of those features is going to be driven by an Angular signal you write over the next four steps.

    Angular's Signals API replaces the pattern of storing state in plain fields and calling markForCheck() or relying on Zone.js to notice a mutation. A signal is a container Angular's change detector already knows how to watch: read it by calling it as a function, write it with .set() or .update(), and every template or computed value that reads it stays in sync automatically. This lab is zoneless - there is no Zone.js patching browser APIs to trigger detection - so signals are not an optimization here, they are the only mechanism that makes the UI update at all. ## Where you will work

    The interfaces you need already exist and will not change:

    • src/models.ts - the CartItem and Product interfaces every task below builds on.

    The files you will complete, in the order you will touch them:

    • src/cart-state.ts - a writable signal holding the cart's contents, plus the update logic and an effect() that reports on it.
    • src/order-summary.ts - a computed() order total and a linkedSignal() shipping choice that resets when the cart changes.
    • src/catalog.resource.ts - an httpResource() that fetches the product catalog, with helpers that read its loading and error state.
    • src/cart.service.ts - an injectable CartService that republishes the cart signal so every component shares one source of truth.
    • src/product-detail.resource.ts - a parameterized httpResource() that only fires once a product id is actually selected.

    Each file already builds and runs; the gaps are marked with // TODO: Task <n> comments that describe exactly what is missing. ## How the scenario maps to the API

    | Storefront feature | Signals concept | File | |---|---|---| | Cart contents update when a shopper adds or removes an item | writable signal(), .update() | cart-state.ts | | An activity log reports every cart change | effect() | cart-state.ts | | The receipt total recalculates without re-summing by hand | computed() | order-summary.ts | | The shipping choice forgets a stale pick once the cart changes | linkedSignal() | order-summary.ts | | The catalog page loads products from the shop API | httpResource() | catalog.resource.ts | | Every page that touches the cart sees the same state | signals inside an injectable service | cart.service.ts | | The product detail page waits for a real id before fetching | a parameterized, conditionally-firing httpResource() | product-detail.resource.ts |

    Each step below completes one row of this table. By the last step the cart signal you build in step 2 is the same signal every later component reads through CartService - nothing gets rebuilt, only reused. ## Solution files

    info> If you get stuck on a task, a reference solution is available. Solution files live in the solution/ folder of your workspace, grouped by step (solution/step2/ through solution/step5/). Each file is named <file>-<step>-<task>.ts, so solution/step2/cart-state-2-1.ts holds the solution for Task 1 in Step 2.

    Try each task on your own first, then compare your work against the matching solution file. Opening a solution file does not change the code you are working on, so copy over only what you need.

  2. Challenge

    Step 2: Create writable signals for the cart and inspect them with effect()

    The cart signal

    A writable signal is created with signal(initialValue) and returns a function: call it with no arguments to read the current value, or call .set(newValue) / .update(fn) on it to change that value. signal.set(x) replaces the value outright; signal.update(fn) derives the new value from whatever the signal currently holds, which matters the moment two changes could otherwise race against a stale read.

    src/cart-state.ts is where the cart's contents live. Open it now - you will find createCartItemsSignal, addItemToCart, and logCartChanges, each with a // TODO marking the missing behavior. With the signal seeding correctly, the next problem is keeping it correct as items come and go. .update() is the right tool whenever the next value depends on the current one - exactly the case when adding a product that might already be in the cart. ## Watching a signal without a template

    A component template reads a signal and Angular reruns the binding automatically, but nothing renders the cart's activity log - it needs the same reactivity without a template in the loop. effect() fills that gap: give it a function, and Angular reruns that function every time a signal it reads changes value, starting with one run right away.

    effect() normally infers where to register itself from the constructor or field initializer it's called in. logCartChanges isn't either - it's a plain function - so it has to be told explicitly where to attach, via the injector option.

  3. Challenge

    Step 3: Derive a memoized order total and a resettable shipping choice

    Deriving state instead of recomputing it

    The checkout summary needs a running total, and recalculating it by hand on every render would mean re-summing the whole cart array even when only the shipping method changed. computed(fn) avoids that: it builds a signal from other signals, reruns fn only when one of those dependencies actually changes, and caches the result in between.

    src/order-summary.ts holds the three signals the checkout summary reads: an order total, a shipping choice, and a free-shipping flag. Start with the total. ## State that resets itself

    The shipping-method picker is different from a plain computed(): it needs to be writable, so a customer can override it, but it also needs to forget that override and fall back to a default the moment the cart itself changes - picking express shipping for one order shouldn't carry over to the next. linkedSignal(computation) is built for exactly this: like computed(), it recomputes when the signals it reads change, but the result is still a writable signal you can .set() on top of.

    linkedSignal() takes a function that reads a source signal and returns the value to reset to. Every time that source's value changes, the linked signal snaps back to whatever the function returns, discarding any manual .set() in between. With a total and a shipping choice in place, the last piece is a flag that depends only on the total - a small, focused computed() that shows exactly how cheaply derived state composes once the underlying signals already exist.

  4. Challenge

    Step 4: Fetch the product catalog with httpResource()

    Fetching data as a signal

    Every signal so far has been driven from local state. httpResource() extends the same model to a network request: it takes a reactive function - one that reads other signals - and returns an object whose value(), isLoading(), and error() are themselves signals, kept in sync with the request's lifecycle. Because the request function is reactive, changing a signal it reads (a search term, a page number, a product id) automatically triggers a new request, with no manual subscription to manage.

    httpResource<Product[]>(() => '/api/products') is correct; httpResource<Product[]>('/api/products') is not - a bare string no longer compiles, the argument must be a function so Angular can re-run it reactively. src/catalog.resource.ts is where the catalog page's data comes from. ## Loading and error states

    A resource's value() is only half the story - the catalog page also needs to know whether a request is still in flight or has failed, so it can show a spinner or an error banner instead of an empty grid. Both are read the same way as value(): resource.isLoading() and resource.error() are ordinary signals, checked in the template or, as here, summarized by a small helper function. defaultValue covers the window before the first response arrives, but value() can still briefly be undefined in edge cases such as a request that gets aborted and restarted. The grid's @for needs an array unconditionally, so the last helper closes that gap.

  5. Challenge

    Step 5: Share signals through CartService and reload resources conditionally

    One cart, many components

    The cart signal from step 2 works fine as long as one component owns it, but Carved Rock Fitness's cart summary lives in the header, the cart page, and the checkout page at once. Recreating the signal in each of them would give each its own independent cart. The fix is the same one you'd reach for with any other shared state: put the signal behind an @Injectable service, so every component that injects it gets a reference to the exact same signal, not a copy.

    src/cart.service.ts declares CartService with cartItems, orderTotal, and shippingMethod fields already wired to the signals you built in steps 2 and 3. What's missing are the two methods components actually call. Adding covers half the interactions the cart page needs; removing an item is the other half, and it has to go through the same signal so orderTotal and shippingMethod - both derived from cartItems - update in lockstep with whatever removed the row. ## Requests that wait for their input

    The product detail page has a problem the catalog page never had: there is no product to fetch until a shopper actually clicks one. If createProductDetailResource requested /api/products/undefined before an id was chosen, it would fire a network call on every navigation to an empty detail page. httpResource() already has a way to say 'not yet': when its reactive function returns undefined, the resource skips the request entirely and simply reports nothing, rather than firing on a placeholder URL.

    src/product-detail.resource.ts takes the product id as a signal for exactly this reason - a signal, unlike a plain string, lets the resource notice the moment a real id arrives and fire only then.

  6. Challenge

    Step 6: Run the application

    Every task is done, and each one was graded the moment you completed it. What you have not done yet is watch the whole application run at once -- which is the only place the pieces you wrote separately become a single working program.

    Start the application

    Open the Terminal tab and start the dev server:

    npm run dev
    

    It serves the application on port 4200. Switch to the Web Browser tab to use it: the page is running the functions you implemented in the previous steps, so what you see is your own code rather than a demonstration of it.

    Leave the server running while you look around. It reloads on save, so you can change a file and see the result without restarting anything. ### Run the checks yourself

    The lab ran each task's test for you as you went, but they are ordinary tests and you can run them whenever you like. The whole suite:

    npx --no-install ng test --no-watch --reporters=dot
    

    Or one task at a time:

    bash runTest.sh "task 2.1"
    

    Each failure names the task it belongs to, so the output tells you which step to go back to rather than which line to stare at. Two things worth trying before you finish: break something on purpose and watch the matching check fail, then put it back. Knowing what a failure looks like is worth as much as knowing what a pass looks like, and it costs you nothing here.

    When you are done, everything you wrote is still in the editor tabs -- the lab is yours to keep reading.

About the author

Pluralsight’s AI authoring technology is designed to accelerate the creation of hands-on, technical learning experiences. Serving as a first-pass content generator, it produces structured lab drafts aligned to learning objectives defined by Pluralsight’s Curriculum team. Each lab is then enhanced by our Content team, who configure the environments, refine instructions, and conduct rigorous technical and quality reviews. The result is a collaboration between artificial intelligence and human expertise, where AI supports scale and efficiency, and Pluralsight experts ensure accuracy, relevance, and instructional quality, helping learners build practical skills with confidence.

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