Share state between Islands

AstroVerse-Astro Islands

When building an Astro website with islands architecture / partial hydration, you may have run into this problem: I want to share state between my components.

UI frameworks like React or Vue may encourage “context” providers for other components to consume. But when partially hydrating components within Astro or Markdown, you can’t use these context wrappers.

Astro recommends a different solution for shared client-side storage: Nano Stores.

<RecipeLinks slugs={[“en/recipes/sharing-state”]} />

Why Nano Stores?

The Nano Stores library allows you to author stores that any component can interact with. We recommend Nano Stores because:

  • They’re lightweight. Nano Stores ship the bare minimum JS you’ll need (less than 1 KB) with zero dependencies.
  • They’re framework-agnostic. This means sharing state between frameworks will be seamless! Astro is built on flexibility, so we love solutions that offer a similar developer experience no matter your preference.

Still, there are a number of alternatives you can explore. These include:

:::note[FAQ]

**🙋 Can I use Nano Stores in `.astro` files or other server-side components?**

Nano Stores can be imported, written to, and read from in server-side components, but we don’t recommend it! This is due to a few restrictions:

  • Writing to a store from a .astro file or non-hydrated component will not affect the value received by client-side components.
  • You cannot pass a Nano Store as a “prop” to client-side components.
  • You cannot subscribe to store changes from a .astro file, since Astro components do not re-render.

If you understand these restrictions and still find a use case, you can give Nano Stores a try! Just remember that Nano Stores are built for reactivity to changes on the client specifically.

**🙋 How do Svelte stores compare to Nano Stores?**

Nano Stores and Svelte stores are very similar! In fact, nanostores allow you to use the same $ shortcut for subscriptions that you might use with Svelte stores.

If you want to avoid third-party libraries, Svelte stores are a great cross-island communication tool on their own. Still, you might prefer Nano Stores if a) you like their add-ons for “objects” and async state, or b) you want to communicate between Svelte and other UI frameworks like Preact or Vue.

**🙋 How do Solid signals compare to Nano Stores?**

If you’ve used Solid for a while, you may have tried moving signals or stores outside of your components. This is a great way to share state between Solid islands! Try exporting signals from a shared file:

// sharedStore.js
import { createSignal } from "solid-js";

export const sharedCount = createSignal(0);

…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” and async state, or b) you want to communicate between Solid and other UI frameworks like Preact or Vue.

:::

Installing Nano Stores

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

```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 ``` ```shell npm install nanostores @nanostores/lit ```

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
import CartFlyoutToggle from "../components/CartFlyoutToggle";
import CartFlyout from "../components/CartFlyout";
import AddToCartForm from "../components/AddToCartForm";
---

<!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
import { atom } from "nanostores";

export const isCartOpen = atom(false);

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 import { useStore } from '@nanostores/preact'; import { isCartOpen } from '../cartStore';

export default function CartButton() { // read the store value with the useStore hook const $isCartOpen = useStore(isCartOpen); // write to the imported store using .set return ( <button onClick={() => isCartOpen.set(!$isCartOpen)}>Cart ) }

</Fragment>
<Fragment slot="react">
```jsx
// src/components/CartFlyoutToggle.jsx
import { useStore } from '@nanostores/react';
import { isCartOpen } from '../cartStore';

export default function CartButton() {
  // read the store value with the `useStore` hook
  const $isCartOpen = useStore(isCartOpen);
  // write to the imported store using `.set`
  return (
    <button onClick={() => isCartOpen.set(!$isCartOpen)}>Cart</button>
  )
}
```jsx // src/components/CartFlyoutToggle.jsx import { useStore } from '@nanostores/solid'; import { isCartOpen } from '../cartStore';

export default function CartButton() { // read the store value with the useStore hook const $isCartOpen = useStore(isCartOpen); // 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>
<Fragment slot="lit">
```ts
// src/components/CartFlyoutToggle.ts
import { LitElement, html } from 'lit';
import { isCartOpen } from '../cartStore';

export class CartFlyoutToggle extends LitElement {
  handleClick() {
    isCartOpen.set(!isCartOpen.get());
  }

  render() {
    return html`
      <button @click="${this.handleClick}">Cart</button>
    `;
  }
}

customElements.define('cart-flyout-toggle', CartFlyoutToggle);

Then, we can read isCartOpen from our CartFlyout component:

```jsx // src/components/CartFlyout.jsx import { useStore } from '@nanostores/preact'; import { isCartOpen } from '../cartStore';

export default function CartFlyout() { const $isCartOpen = useStore(isCartOpen);

return $isCartOpen ?

: null; }

</Fragment>
<Fragment slot="react">
```jsx
// src/components/CartFlyout.jsx
import { useStore } from '@nanostores/react';
import { isCartOpen } from '../cartStore';

export default function CartFlyout() {
  const $isCartOpen = useStore(isCartOpen);

  return $isCartOpen ? <aside>...</aside> : null;
}
```jsx // src/components/CartFlyout.jsx import { useStore } from '@nanostores/solid'; import { isCartOpen } from '../cartStore';

export default function CartFlyout() { const $isCartOpen = useStore(isCartOpen);

return $isCartOpen() ?

: null; }

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

{#if $isCartOpen}
<aside>...</aside>
{/if}
```vue
</Fragment>
<Fragment slot="lit">
```ts
// src/components/CartFlyout.ts
import { isCartOpen } from '../cartStore';
import { LitElement, html } from 'lit';
import { StoreController } from '@nanostores/lit';

export class CartFlyout extends LitElement {
  private cartOpen = new StoreController(this, isCartOpen);

  render() {
    return this.cartOpen.value ? html`<aside>...</aside>` : null;
  }
}

customElements.define('cart-flyout', CartFlyout);

Using “maps”

:::tip 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 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.

```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({});

</Fragment>
<Fragment slot="ts">
```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>>({});

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.
```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 import { addCartItem, isCartOpen } from '../cartStore';

export default function AddToCartForm({ children }) { // we’ll hardcode the item info for simplicity! const hardcodedItemInfo = { id: ‘astronaut-figurine’, name: ‘Astronaut Figurine’, imageSrc: ‘/images/astronaut-figurine.png’, }

function addToCart(e) { e.preventDefault(); isCartOpen.set(true); addCartItem(hardcodedItemInfo); }

return (

{children}
) }
</Fragment>
<Fragment slot="react">
```jsx
// src/components/AddToCartForm.jsx
import { addCartItem, isCartOpen } from '../cartStore';

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

  function addToCart(e) {
    e.preventDefault();
    isCartOpen.set(true);
    addCartItem(hardcodedItemInfo);
  }

  return (
    <form onSubmit={addToCart}>
      {children}
    </form>
  )
}
```jsx // src/components/AddToCartForm.jsx import { addCartItem, isCartOpen } from '../cartStore';

export default function AddToCartForm({ children }) { // we’ll hardcode the item info for simplicity! const hardcodedItemInfo = { id: ‘astronaut-figurine’, name: ‘Astronaut Figurine’, imageSrc: ‘/images/astronaut-figurine.png’, }

function addToCart(e) { e.preventDefault(); 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>
<Fragment slot="lit">
```ts
// src/components/AddToCartForm.ts
import { LitElement, html } from 'lit';
import { isCartOpen, addCartItem } from '../cartStore';

export class AddToCartForm extends LitElement {
  static get properties() {
    return {
      item: { type: Object },
    };
  }

  constructor() {
    super();
    this.item = {};
  }

  addToCart(e) {
    e.preventDefault();
    isCartOpen.set(true);
    addCartItem(this.item);
  }

  render() {
    return html`
      <form @submit="${this.addToCart}">
        <slot></slot>
      </form>
    `;
  }
}
customElements.define('add-to-cart-form', AddToCartForm);

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

```jsx // src/components/CartFlyout.jsx import { useStore } from '@nanostores/preact'; import { isCartOpen, cartItems } from '../cartStore';

export default function CartFlyout() { const $isCartOpen = useStore(isCartOpen); const $cartItems = useStore(cartItems);

return $isCartOpen ? (

) : null; }
</Fragment>
<Fragment slot="react">
```jsx
// src/components/CartFlyout.jsx
import { useStore } from '@nanostores/react';
import { isCartOpen, cartItems } from '../cartStore';

export default function CartFlyout() {
  const $isCartOpen = useStore(isCartOpen);
  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 import { useStore } from '@nanostores/solid'; import { isCartOpen, cartItems } from '../cartStore';

export default function CartFlyout() { const $isCartOpen = useStore(isCartOpen); const $cartItems = useStore(cartItems);

return $isCartOpen() ? (

) : null; }
</Fragment>
<Fragment slot="svelte">
```svelte
<!--src/components/CartFlyout.svelte-->
<script>
  import { isCartOpen, cartItems } from '../cartStore';
</script>

{#if $isCartOpen}
  {#if Object.values($cartItems).length}
    <aside>
      {#each Object.values($cartItems) as cartItem}
      <li>
        <img src={cartItem.imageSrc} alt={cartItem.name} />
        <h3>{cartItem.name}</h3>
        <p>Quantity: {cartItem.quantity}</p>
      </li>
      {/each}
    </aside>
  {#else}
    <p>Your cart is empty!</p>
  {/if}
{/if}
```vue
</Fragment>
<Fragment slot="lit">
```ts
// src/components/CartFlyout.ts
import { LitElement, html } from 'lit';
import { isCartOpen, cartItems } from '../cartStore';
import { StoreController } from '@nanostores/lit';

export class CartFlyoutLit extends LitElement {
  private cartOpen = new StoreController(this, isCartOpen);
  private getCartItems = new StoreController(this, cartItems);

  renderCartItem(cartItem) {
    return html`
      <li>
        <img src="${cartItem.imageSrc}" alt="${cartItem.name}" />
        <h3>${cartItem.name}</h3>
        <p>Quantity: ${cartItem.quantity}</p>
      </li>
    `;
  }

  render() {
    return this.cartOpen.value
      ? html`
          <aside>
            ${
              Object.values(this.getCartItems.value).length
                ? html`
                  <ul>
                    ${Object.values(this.getCartItems.value).map((cartItem) =>
                      this.renderCartItem(cartItem)
                    )}
                  </ul>
                `
                : html`<p>Your cart is empty!</p>`
            }
          </aside>
        `
      : null;
  }
}

customElements.define('cart-flyout', CartFlyoutLit);

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!

Publish on 2023-09-06,Update on 2024-11-19