[Guide] Build an E-commerce Storefront - implementation notes
The Storefront guide is a strong example of the repo's philosophy applied to commerce: keep the file map calm, keep checkout motion in route state, and make the cart a global vault with local mirrors so the app stays consistent across routes. It also treats derived totals (subtotal/tax/total) as a debugging surface, not just a performance trick.
Did you actually mirror the cart into each route, or did you read the vault directly and accept slower renders? Where do you keep checkout stepper truth (route keys vs URL vs document), and how do you make step transitions reversible? How do you prevent cart totals from drifting when different panels write items/coupons? If you're doing "freshness by default" on product grids, what do you render as evidence that a grid is fresh vs cached?
Comments (16)
Back to latestThe mirror approach was better than I expected.
Reading the vault directly made the cart badge feel laggy whenever a route rerendered under load.
We treated checkout step as pure route truth and made it self-correct in render.
If step=payment but the cart is empty, we write step=cart and jump. It's blunt but it avoids dead ends.
Counterpoint: a global cart vault can become "one giant store" very quickly.
Once you add shipping address, promo rules, and inventory flags, the cart starts owning half the app.
Agree. We kept the vault surface intentionally small and stored only keys that multiple routes needed.
Everything else lived in a checkout document keyed by session id, and we rendered that id as evidence.
Storing derived totals saved us in support tickets.
Users would screenshot "total wrong" and we'd see subtotal=… tax=… and immediately know which lane was broken (item pricing vs tax calc).
We made totals derivation a single helper and called it in the mutation path:
ts
function applyCartMutation(vault: any, nextItems: any[]) {
vault.write('items', nextItems);
const subtotal = nextItems.reduce((s: number, it: any) => s + it.price * it.qty, 0);
const tax = Math.round(subtotal * 0.08);
vault.write('subtotal', subtotal);
vault.write('tax', tax);
vault.write('total', subtotal + tax);
vault.write('lastIntent', { type: 'reprice', at: Date.now() });
}
Derivation in one place made it hard to forget updates when coupons changed.
On product grid freshness: we rendered a data-freshness attribute and an ISO timestamp of last read in the grid header.
People stopped reporting "stale results" once they could see when the grid last refreshed.
A misunderstanding I had at first: I assumed checkout step should be in the URL path.
Route state as stepper truth is nicer because you can keep the map calm and still express a lot of motion.
Mirrors helped us avoid tearing when multiple parts of the UI needed cart count.
If each panel reads vault and derives count, you can get subtle inconsistencies on fast updates.
We rendered data-currency and data-cart-items on the shell and it made pricing bugs much easier to reproduce.
Commerce bugs are often configuration bugs, not component bugs.
Checkout reversibility mattered more than I expected.
We kept a lastStepIntent in route state so back/forward + stepper clicks were explainable.
Docs tie-in: this guide is basically "state is a contract" applied to commerce.
Related: State Management (Best Practices) and Routing and Navigation.
We treated coupons as intent objects rather than strings so we could log why a coupon was rejected.
It fits the guide posture: if there's a rule, it should be visible.
Counterpoint: storing derived totals can hide pricing bugs if the source items are wrong but totals look consistent.
We also rendered a derived "pricing evidence" string that included item ids + qty + computed line totals.
The guide convinced me that cart consistency is a UX feature.
Even if the totals are "wrong" for a moment, a consistent story is less confusing than flicker.
Has anyone tried using route session slugs for checkout (like the calendar guide does for editing)?
We did and it made it easier to resume checkout, but it added a lot of session cleanup work.
We did session slugs too and ended up treating them like a document identity (expire + fork).
Once you render the session id as evidence, the complexity is at least understandable.
The calm map + busy motion approach is the part I'd keep even if you change everything else.
It makes commerce flows feel like an app, not like a series of page loads.