Skip to content

← all comparisons

Local UI state + persistent global store

Wishlist toggle

A heart-shaped toggle on each product card that persists across sessions. The button reads "is this in the wishlist?" from a derived value and toggles the entry on click — same shape as the cart, but the store is a Set-of-ids and the state read is a boolean.

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 453 B gzip · 26 lines
WishlistButtonReact.tsx
How this works

useStore(wishlist) gives the current map; saved is a plain boolean derived inline. aria-pressed on the button + class swap based on saved is the entire visual state.

import { useStore } from '@nanostores/react';
import { wishlist, toggleWishlist } from '../stores/wishlist';

type Props = { id: number };

export default function WishlistButtonReact({ id }: Props) {
  const $wishlist = useStore(wishlist);
  const saved = !!$wishlist[id];

  return (
    <button
      type="button"
      onClick={() => toggleWishlist(id)}
      aria-label={saved ? 'Remove from wishlist' : 'Save to wishlist'}
      aria-pressed={saved}
      className={`flex h-8 w-8 items-center justify-center rounded-full border text-base transition ${
        saved
          ? 'border-rose-300 bg-rose-50 text-rose-600'
          : 'border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 text-slate-400 hover:text-rose-500'
      }`}
    >
      {saved ? '♥' : '♡'}
    </button>
  );
}
Vue 469 B gzip · 27 lines
WishlistButtonVue.vue
How this works

useStore returns a Vue ref; computed wraps the boolean lookup so the template auto-tracks it. :class array switches between two Tailwind sets.

<script setup lang="ts">
import { computed } from 'vue';
import { useStore } from '@nanostores/vue';
import { wishlist, toggleWishlist } from '../stores/wishlist';

const props = defineProps<{ id: number }>();
const $wishlist = useStore(wishlist);
const saved = computed(() => !!$wishlist.value[props.id]);
</script>

<template>
  <button
    type="button"
    @click="toggleWishlist(props.id)"
    :aria-label="saved ? 'Remove from wishlist' : 'Save to wishlist'"
    :aria-pressed="saved"
    :class="[
      'flex h-8 w-8 items-center justify-center rounded-full border text-base transition',
      saved
        ? 'border-rose-300 bg-rose-50 text-rose-600'
        : 'border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 text-slate-400 hover:text-rose-500',
    ]"
  >
    {{ saved ? '♥' : '♡' }}
  </button>
</template>
Svelte 403 B gzip · 21 lines
WishlistButtonSvelte.svelte
How this works

$wishlist auto-subscription, then saved is a $derived rune over the lookup. Pure runes, no adapter — same five lines you would write to read any other store.

<script lang="ts">
  import { wishlist, toggleWishlist } from '../stores/wishlist';

  let { id }: { id: number } = $props();
  const saved = $derived(!!$wishlist[id]);
</script>

<button
  type="button"
  onclick={() => toggleWishlist(id)}
  aria-label={saved ? 'Remove from wishlist' : 'Save to wishlist'}
  aria-pressed={saved}
  class={`flex h-8 w-8 items-center justify-center rounded-full border text-base transition ${
    saved
      ? 'border-rose-300 bg-rose-50 text-rose-600'
      : 'border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 text-slate-400 hover:text-rose-500'
  }`}
>
  {saved ? '♥' : '♡'}
</button>