- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
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 Info
Table of Contents
-
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 workThe interfaces you need already exist and will not change:
src/models.ts- theCartItemandProductinterfaces 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 aneffect()that reports on it.src/order-summary.ts- acomputed()order total and alinkedSignal()shipping choice that resets when the cart changes.src/catalog.resource.ts- anhttpResource()that fetches the product catalog, with helpers that read its loading and error state.src/cart.service.ts- an injectableCartServicethat republishes the cart signal so every component shares one source of truth.src/product-detail.resource.ts- a parameterizedhttpResource()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-firinghttpResource()|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 filesinfo> 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/throughsolution/step5/). Each file is named<file>-<step>-<task>.ts, sosolution/step2/cart-state-2-1.tsholds 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.
-
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.tsis where the cart's contents live. Open it now - you will findcreateCartItemsSignal,addItemToCart, andlogCartChanges, each with a// TODOmarking 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 templateA 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.logCartChangesisn't either - it's a plain function - so it has to be told explicitly where to attach, via theinjectoroption. -
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, rerunsfnonly when one of those dependencies actually changes, and caches the result in between.src/order-summary.tsholds 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 itselfThe 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: likecomputed(), 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, focusedcomputed()that shows exactly how cheaply derived state composes once the underlying signals already exist. -
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 whosevalue(),isLoading(), anderror()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.tsis where the catalog page's data comes from. ## Loading and error statesA 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 asvalue():resource.isLoading()andresource.error()are ordinary signals, checked in the template or, as here, summarized by a small helper function.defaultValuecovers the window before the first response arrives, butvalue()can still briefly beundefinedin edge cases such as a request that gets aborted and restarted. The grid's@forneeds an array unconditionally, so the last helper closes that gap. -
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
@Injectableservice, so every component that injects it gets a reference to the exact same signal, not a copy.src/cart.service.tsdeclaresCartServicewithcartItems,orderTotal, andshippingMethodfields 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 soorderTotalandshippingMethod- both derived fromcartItems- update in lockstep with whatever removed the row. ## Requests that wait for their inputThe product detail page has a problem the catalog page never had: there is no product to fetch until a shopper actually clicks one. If
createProductDetailResourcerequested/api/products/undefinedbefore 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 returnsundefined, the resource skips the request entirely and simply reports nothing, rather than firing on a placeholder URL.src/product-detail.resource.tstakes 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. -
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 devIt 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=dotOr 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
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.