Sharing State

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.

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 Preact, Svelte, and Vue 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:

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

npm i nanostores @nanostores/preact

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

Usage example - ecommerce cart flyout

Section titled 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

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>

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:

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</button>
  )
}

Then, we can read isCartOpen from our CartFlyout component:

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

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

  return $isCartOpen ? <aside>...</aside> : null;
}

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.

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

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

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.

src/components/AddToCartForm.jsx
import { addCartItem, isCartOpen } from './cart/store';

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

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

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 ? (
    <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;
}

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!