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.
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>
);
} 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> 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>