Skip to content

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 (useEffect vs onMounted/onUnmounted vs $effect).
  • Product carousel — DOM refs (useRef vs template ref vs bind:this).
  • Product list — async fetch state machine + derived filter (useMemo vs computed vs $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 onChange vs v-model.lazy vs bind: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 $store auto-subscription. Persisted to localStorage via @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 unreachableDummyJSON. Frontend samples every 12th item from ?limit=100 for 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.