About
shop-three-ways is a small e-commerce demo where every feature is implemented three times — in React, Vue, and Svelte — and rendered side-by-side as Astro islands. The point is to make the three framework idioms readable at a glance, so a developer fluent in one can pick up the other two without translating between mental models.
Who it's for
Anyone who's confident in one of {React, Vue, Svelte} and wants a Rosetta-stone-style intro to the other two. The reading mode is: find the column you already know, then read the same feature in the other two columns next to it.
How it's structured
A working shop comes first, with a Rosetta reference layered on top:
- /shop — three product lists fetching the same API, sharing one cart.
- /cart — three views of the same cart, each with the framework's two-way binding idiom on the qty input.
- / — the home page hosts the slide-banner (timers + cleanup) and product-carousel (DOM refs) demos.
- /compare — for every feature, the three real source files side-by-side, syntax-highlighted. Always in sync with the live shop because they're loaded with
?raw.
The concepts each feature isolates
- Slide banner — timers + cleanup (
useEffectvsonMounted/onUnmountedvs$effect). - Product carousel — DOM refs (
useRefvs template ref vsbind:this). - Product list — async fetch state machine + derived filter (
useMemovscomputedvs$derived). - Add-to-cart + cart badge — global state via nanostores. The same atom is read and written from all three frameworks.
- Quantity stepper — two-way binding (controlled
onChangevsv-model.lazyvsbind:value+onchange). - Wishlist toggle + toast notifications — local UI state on a persistent store, plus the composition primitive each framework uses (
children/<slot />/Snippet).
Which framework would we recommend
The honest answer: Svelte. Not because the
other two are wrong — they're not — but because the same shop
ships in noticeably less code on the Svelte side, the runes
($state, $derived, $effect)
compose without the dependency-array footguns of useEffect
or the .value ceremony of Vue refs, and the runtime
overhead is small enough to ignore.
We voted with our own code: the site's singleton chrome — theme toggle and toast container — is Svelte, even though all three implementations exist in the repo. The cross-framework comparisons (cart badges in the header, every page under /compare) stay multi-framework because that's the point of the site; anything else defaults to Svelte.
If you came here fluent in React or Vue and want to keep shipping in those, this site still helps — read your own column first, then read the Svelte one. If after that you want to try Svelte, the same rune syntax used here is the same one you'd use on day one of a real project.
Tech stack
- Astro 6 as the multi-framework host.
- React 19, Vue 3.5, Svelte 5 (runes) — all latest idiomatic syntax.
- nanostores as the framework-agnostic store, with
@nanostores/react,@nanostores/vue, and Svelte's native$storeauto-subscription. Persisted tolocalStoragevia@nanostores/persistent. - Tailwind CSS v4 (Vite plugin) so the visual layer is identical across the three columns.
- TypeScript everywhere, kept minimal.
- Astro's built-in
<Code>component (Shiki) for the source-comparison pages. - Singleton islands (theme toggle, toast container) are Svelte. See the recommendation for why.
A note on the size badges
Each /compare panel shows the source file's gzipped
byte size and line count. That measures the component file you
wrote — useful for "how much code did this take?" — not the
runtime cost paid in the browser. Framework runtime is shared
across every island on the page (rough orders: React ~45 KB,
Vue ~35 KB, Svelte ~5 KB gzipped) and isn't reflected in the
per-component badges.
Data source
Products come from one of two places, picked at build/load time
by the PUBLIC_API_BASE env var:
- Set (e.g.
http://localhost:8080) → fetch from the sibling shop-two-backends Go (:8080) or Rust (:8081) service. 33 products, server-assigned slugs, no reviews. - Unset or unreachable →
DummyJSON.
Frontend samples every 12th item from
?limit=100for category variety; slugs are generated from titles. This is the standalone-deploy fallback so the site works without standing up the backends.
Both paths normalize through fetchProducts() in
src/data/products.ts, so every framework's product
list and carousel calls one helper. Each call on mount is the
demo's async/effects exercise.
A note on framework versions
The Svelte column uses Svelte 5 runes ($state,
$derived, $effect, $props),
which is a meaningful syntactic shift from older tutorials. The
React column uses React 19 conventions, and Vue uses
<script setup> with the Composition API.
Older syntax works but is intentionally excluded so each column
shows the framework's current best practice.