Skip to content

← all comparisons

Logic composition (reusable reactive logic)

useCart — hook / composable / rune function

Cart subtotal lookup is needed in both CartView and CheckoutForm. Each framework has its own pattern for extracting reactive logic into a reusable function — different name, same idea. The three live in src/hooks, src/composables, and src/lib so they read like real-world code rather than demo files.

Each panel header shows the source size of that component file (raw, gzipped, line count). Source-only — framework runtime (React ~45 KB, Vue ~35 KB, Svelte ~5 KB gzipped) is shared across every island and isn't included in these numbers.

React 391 B gzip · 25 lines
useCartReact.ts
How this works

A "custom hook" — a function whose name starts with use that internally calls other hooks. useStore subscribe" class="text-blue-700 underline decoration-blue-200 underline-offset-2 hover:decoration-blue-500">subscribes the calling component to the cart atom; useMemo recomputes items / subtotal / count only when the underlying $cart changes. Returning a frozen object keeps consumers from re-rendering on shape mutations.

import { useMemo } from 'react';
import { useStore } from '@nanostores/react';
import { cart, type CartItem } from '../stores/cart';

export type CartTotals = {
  items: CartItem[];
  subtotal: number;
  count: number;
};

/**
 * Custom hook: subscribes to the cart store and returns the items list,
 * subtotal, and total quantity. The useMemo guards against re-allocating
 * the totals object on unrelated re-renders.
 */
export function useCart(): CartTotals {
  const $cart = useStore(cart);
  return useMemo(() => {
    const items = Object.values($cart);
    const subtotal = items.reduce((s, i) => s + i.price * i.qty, 0);
    const count = items.reduce((s, i) => s + i.qty, 0);
    return { items, subtotal, count };
  }, [$cart]);
}
Vue 399 B gzip · 27 lines
useCartVue.ts
How this works

A "composable" — a plain function that returns refs / computeds. The Vue analogue of a custom hook, popularized by the Composition API. Templates auto-unwrap refs, so consumers can write {{ subtotal }} or :class without .value. The composable convention is alphabetic: any file under src/composables/ that starts with use.

import { computed, type ComputedRef } from 'vue';
import { useStore } from '@nanostores/vue';
import { cart, type CartItem } from '../stores/cart';

export type CartTotals = {
  items: ComputedRef<CartItem[]>;
  subtotal: ComputedRef<number>;
  count: ComputedRef<number>;
};

/**
 * Composable: subscribes to the cart store and exposes items / subtotal /
 * count as Vue refs so consumers can read them directly in templates and
 * track them in their own computed values.
 */
export function useCart(): CartTotals {
  const $cart = useStore(cart);
  const items = computed(() => Object.values($cart.value));
  const subtotal = computed(() =>
    items.value.reduce((s, i) => s + i.price * i.qty, 0),
  );
  const count = computed(() =>
    items.value.reduce((s, i) => s + i.qty, 0),
  );
  return { items, subtotal, count };
}
Svelte 506 B gzip · 37 lines
useCartSvelte.svelte.ts
How this works

Svelte 5 lets you use runes ($state, $derived, $effect) in any file ending in .svelte.js or .svelte.ts. fromStore() bridges a regular Svelte store (nanostores conform) into a reactive { current } shape. The function returns getters because $derived values are live — consuming a getter in a component or another rune-using function tracks the read.

import { fromStore } from 'svelte/store';
import { cart, type CartItem } from '../stores/cart';

/**
 * A rune-using function. Svelte 5 lets you use $state / $derived / $effect
 * outside .svelte components if the file ends in .svelte.ts (or .svelte.js).
 *
 * fromStore wraps a regular Svelte-compatible store (nanostores qualify) so
 * its current value is reactive. The returned object uses getters because
 * $derived values are "live" — bare values would freeze at the call site.
 */
export function useCart(): {
  readonly items: CartItem[];
  readonly subtotal: number;
  readonly count: number;
} {
  const cartRef = fromStore(cart);
  const items = $derived(Object.values(cartRef.current));
  const subtotal = $derived(
    items.reduce((s, i) => s + i.price * i.qty, 0),
  );
  const count = $derived(
    items.reduce((s, i) => s + i.qty, 0),
  );
  return {
    get items() {
      return items;
    },
    get subtotal() {
      return subtotal;
    },
    get count() {
      return count;
    },
  };
}