Skip to content

astro

Share state between islands

Astro

Share state between islands

Share state between islands

...and all components importing `sharedCount` will share the same state. Though this works well, you might prefer Nano Stores if a) you like their add-ons for ["objects"](https://github.com/nanostores/nanostores#maps) and [async state](https://github.com/nanostores/nanostores#lazy-stores), or b) you want to communicate between Solid and other UI frameworks like Preact or Vue.
</details>
:::

## Installing Nano Stores

To get started, install Nano Stores alongside their helper package for your favorite UI framework:

<UIFrameworkTabs>
  <Fragment slot="preact">
  ```shell
  npm install nanostores @nanostores/preact
```shell npm install nanostores @nanostores/react ``` ```shell npm install nanostores @nanostores/solid ``` ```shell npm install nanostores ``` :::note No helper package here! Nano Stores can be used like standard Svelte stores. ::: ```shell npm install nanostores @nanostores/vue ```

You can jump into the Nano Stores usage guide from here, or follow along with our example below!

Usage example - ecommerce cart flyout

Let's say we're building a simple ecommerce interface with three interactive elements:

  • An "add to cart" submission form
  • A cart flyout to display those added items
  • A cart flyout toggle

<LoopingVideo sources={[{ src: '/videos/stores-example.mp4', type: 'video/mp4' }]} />

Try the completed example on your machine or online via StackBlitz.

Your base Astro file may look like this:

---
// src/pages/index.astro



---

<!DOCTYPE html>
<html lang="en">
<head>...</head>
<body>
  <header>
    <nav>
      <a href="/">Astro storefront</a>
      <CartFlyoutToggle client:load />
    </nav>
  </header>
  <main>
    <AddToCartForm client:load>
    <!-- ... -->
    </AddToCartForm>
  </main>
  <CartFlyout client:load />
</body>
</html>

Using "atoms"

Let's start by opening our CartFlyout whenever CartFlyoutToggle is clicked.

First, create a new JS or TS file to contain our store. We'll use an "atom" for this:

// src/cartStore.js

Now, we can import this store into any file that needs to read or write. We'll start by wiring up our CartFlyoutToggle:

```jsx // src/components/CartFlyoutToggle.jsx

// write to the imported store using .set return ( <button onClick={() => isCartOpen.set(!$isCartOpen)}>Cart ) }

</Fragment>
<Fragment slot="react">
```jsx
// src/components/CartFlyoutToggle.jsx



  // write to the imported store using `.set`
  return (
    <button onClick={() => isCartOpen.set(!$isCartOpen)}>Cart</button>
  )
}
```jsx // src/components/CartFlyoutToggle.jsx

// write to the imported store using .set return ( <button onClick={() => isCartOpen.set(!$isCartOpen())}>Cart ) }

</Fragment>
<Fragment slot="svelte">
```svelte
<!--src/components/CartFlyoutToggle.svelte-->
<script>
  import { isCartOpen } from '../cartStore';
</script>

<!--use "$" to read the store value-->
<button on:click={() => isCartOpen.set(!$isCartOpen)}>Cart</button>
```vue
</Fragment>

</UIFrameworkTabs>

Then, we can read `isCartOpen` from our `CartFlyout` component:

<UIFrameworkTabs>
<Fragment slot="preact">
```jsx
// src/components/CartFlyout.jsx



  return $isCartOpen ? <aside>...</aside> : null;
}
```jsx // src/components/CartFlyout.jsx

return $isCartOpen ?

: null; }

</Fragment>
<Fragment slot="solid">
```jsx
// src/components/CartFlyout.jsx



  return $isCartOpen() ? <aside>...</aside> : null;
}
```svelte

{#if $isCartOpen}

{/if} ```
```vue
</Fragment>

</UIFrameworkTabs>

### Using "maps"

:::tip
**[Maps](https://github.com/nanostores/nanostores#maps) are a great choice for objects you write to regularly!** Alongside the standard `get()` and `set()` helpers an `atom` provides, you'll also have a `.setKey()` function to efficiently update individual object keys.
:::

Now, let's keep track of the items inside your cart. To avoid duplicates and keep track of "quantity," we can store your cart as an object with the item's ID as a key. We'll use a [Map](https://github.com/nanostores/nanostores#maps) for this.

Let's add a `cartItem` store to our `cartStore.js` from earlier. You can also switch to a TypeScript file to define the shape if you're so inclined.

<JavascriptFlavorTabs>
  <Fragment slot="js">
  ```js
  // src/cartStore.js
  import { atom, map } from 'nanostores';

  export const isCartOpen = atom(false);

  /**
   * @typedef {Object} CartItem
   * @property {string} id
   * @property {string} name
   * @property {string} imageSrc
   * @property {number} quantity
   */

  /** @type {import('nanostores').MapStore<Record<string, CartItem>>} */
  export const cartItems = map({});
```ts // src/cartStore.ts import { atom, map } from 'nanostores';

export const isCartOpen = atom(false);

export type CartItem = { id: string; name: string; imageSrc: string; quantity: number; }

export const cartItems = map<Record<string, CartItem>>({});

</Fragment>
</JavascriptFlavorTabs>

Now, let's export an `addCartItem` helper for our components to use.
- **If that item doesn't exist in your cart**, add the item with a starting quantity of 1.
- **If that item _does_ already exist**, bump the quantity by 1.

<JavascriptFlavorTabs>
<Fragment slot="js">
```js
// src/cartStore.js
...
export function addCartItem({ id, name, imageSrc }) {
  const existingEntry = cartItems.get()[id];
  if (existingEntry) {
    cartItems.setKey(id, {
      ...existingEntry,
      quantity: existingEntry.quantity + 1,
    })
  } else {
    cartItems.setKey(
      id,
      { id, name, imageSrc, quantity: 1 }
    );
  }
}
```ts // src/cartStore.ts ... type ItemDisplayInfo = Pick; export function addCartItem({ id, name, imageSrc }: ItemDisplayInfo) { const existingEntry = cartItems.get()[id]; if (existingEntry) { cartItems.setKey(id, { ...existingEntry, quantity: existingEntry.quantity + 1, }); } else { cartItems.setKey( id, { id, name, imageSrc, quantity: 1 } ); } } ```

:::note

**🙋 Why use `.get()` here instead of a `useStore` helper?**

You may have noticed we're calling cartItems.get() here, instead of grabbing that useStore helper from our React / Preact / Solid / Vue examples. This is because useStore is meant to trigger component re-renders. In other words, useStore should be used whenever the store value is being rendered to the UI. Since we're reading the value when an event is triggered (addToCart in this case), and we aren't trying to render that value, we don't need useStore here.

:::

With our store in place, we can call this function inside our AddToCartForm whenever that form is submitted. We'll also open the cart flyout so you can see a full cart summary.

```jsx // src/components/AddToCartForm.jsx
isCartOpen.set(true);
addCartItem(hardcodedItemInfo);

}

return (

{children}
) }

</Fragment>
<Fragment slot="react">
```jsx
// src/components/AddToCartForm.jsx


    isCartOpen.set(true);
    addCartItem(hardcodedItemInfo);
  }

  return (
    <form onSubmit={addToCart}>
      {children}
    </form>
  )
}
```jsx // src/components/AddToCartForm.jsx
isCartOpen.set(true);
addCartItem(hardcodedItemInfo);

}

return (

{children}
) }

</Fragment>
<Fragment slot="svelte">
```svelte
<!--src/components/AddToCartForm.svelte-->
<form on:submit|preventDefault={addToCart}>
  <slot></slot>
</form>

<script>
  import { addCartItem, isCartOpen } from '../cartStore';

  // we'll hardcode the item info for simplicity!
  const hardcodedItemInfo = {
    id: 'astronaut-figurine',
    name: 'Astronaut Figurine',
    imageSrc: '/images/astronaut-figurine.png',
  }

  function addToCart() {
    isCartOpen.set(true);
    addCartItem(hardcodedItemInfo);
  }
</script>
```vue
</Fragment>

</UIFrameworkTabs>

Finally, we'll render those cart items inside our `CartFlyout`:

<UIFrameworkTabs>
<Fragment slot="preact">
```jsx
// src/components/CartFlyout.jsx



  const $cartItems = useStore(cartItems);

  return $isCartOpen ? (
    <aside>
      {Object.values($cartItems).length ? (
        <ul>
          {Object.values($cartItems).map(cartItem => (
            <li>
              <img src={cartItem.imageSrc} alt={cartItem.name} />
              <h3>{cartItem.name}</h3>
              <p>Quantity: {cartItem.quantity}</p>
            </li>
          ))}
        </ul>
      ) : <p>Your cart is empty!</p>}
    </aside>
  ) : null;
}
```jsx // src/components/CartFlyout.jsx

const $cartItems = useStore(cartItems);

return $isCartOpen ? (

) : null; }

</Fragment>
<Fragment slot="solid">
```jsx
// src/components/CartFlyout.jsx



  const $cartItems = useStore(cartItems);

  return $isCartOpen() ? (
    <aside>
      {Object.values($cartItems()).length ? (
        <ul>
          {Object.values($cartItems()).map(cartItem => (
            <li>
              <img src={cartItem.imageSrc} alt={cartItem.name} />
              <h3>{cartItem.name}</h3>
              <p>Quantity: {cartItem.quantity}</p>
            </li>
          ))}
        </ul>
      ) : <p>Your cart is empty!</p>}
    </aside>
  ) : null;
}
```svelte

{#if $isCartOpen} {#if Object.values($cartItems).length}

{:else}

Your cart is empty!

{/if} {/if}

</Fragment>
<Fragment slot="vue">
```vue
<!--src/components/CartFlyout.vue-->
<template>
  <aside v-if="$isCartOpen">
    <ul v-if="Object.values($cartItems).length">
      <li v-for="cartItem in Object.values($cartItems)" v-bind:key="cartItem.name">
        <img :src=cartItem.imageSrc :alt=cartItem.name />
        <h3>{{cartItem.name}}</h3>
        <p>Quantity: {{cartItem.quantity}}</p>
      </li>
    </ul>
    <p v-else>Your cart is empty!</p>
  </aside>
</template>

<script setup>
  import { cartItems, isCartOpen } from '../cartStore';
  import { useStore } from '@nanostores/vue';

  const $isCartOpen = useStore(isCartOpen);
  const $cartItems = useStore(cartItems);
</script>

Now, you should have a fully interactive ecommerce example with the smallest JS bundle in the galaxy 🚀

Try the completed example on your machine or online via StackBlitz!