Zum Inhalt springen

Zustand zwischen Astro-Inseln teilen

Wenn du eine Astro-Website mit Insel-Architektur / Partieller Hydratation aufbaust, bist du vielleicht schon auf dieses Problem gestoßen: Ich möchte den Zustand zwischen meinen Komponenten teilen.

UI-Frameworks wie React oder Vue nutzen “Kontext”-Anbieter, die von anderen Komponenten konsumiert werden können. Bei der partiellen Hydratation von Komponenten in Astro oder Markdown kannst du diese Kontext-Wrapper jedoch nicht verwenden.

Astro empfiehlt eine andere Lösung für die gemeinsame Speicherung auf der Client-Seite: Nano Stores.

Die Nano Stores Bibliothek ermöglicht es dir, Stores zu erstellen, mit denen jede Komponente interagieren kann. Wir empfehlen Nano Stores aus folgenden Gründen:

  • Sie sind leichtgewichtig. Nano Stores liefern das Minimum an JS, das du brauchst (weniger als 1 KB), ohne jegliche Abhängigkeiten.
  • Sie sind Framework-agnostisch. Das bedeutet, dass die gemeinsame Nutzung von Zuständen zwischen Frameworks nahtlos ist! Astro ist auf Flexibilität ausgelegt, daher lieben wir Lösungen, die unabhängig von deiner Präferenz eine ähnliche Entwicklererfahrung bieten.

Dennoch gibt es eine Reihe von Alternativen, die du ausprobieren kannst. Dazu gehören:

Um loszulegen, installiere Nano Stores zusammen mit dem Hilfspaket für dein bevorzugtes UI-Framework:

Terminal-Fenster
npm install nanostores @nanostores/preact

Du kannst die Nano-Stores-Anleitung von hier aus aufrufen oder unserem Beispiel unten folgen!

Anwendungsbeispiel - E-Commerce Warenkorb-Flyout

Abschnitt betitelt Anwendungsbeispiel - E-Commerce Warenkorb-Flyout

Nehmen wir an, wir bauen eine einfache E-Commerce-Oberfläche mit drei interaktiven Elementen:

  • Ein “In den Warenkorb”-Formular
  • Ein Warenkorb-Flyout, um die hinzugefügten Artikel anzuzeigen
  • Ein Flyout-Knopf für den Einkaufswagen

Teste das fertige Beispiel auf deinem Rechner oder online über Stackblitz.

Deine Basis-Astro-Datei könnte so aussehen:

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-Schaufenster</a>
<CartFlyoutToggle client:load />
</nav>
</header>
<main>
<AddToCartForm client:load>
<!-- ... -->
</AddToCartForm>
</main>
<CartFlyout client:load />
</body>
</html>

Beginnen wir damit, dass wir unser CartFlyout öffnen, wenn CartFlyoutToggle angeklickt wird.

Erstelle zunächst eine neue JS- oder TS-Datei, die unseren Store enthält. Wir verwenden dafür ein “Atom”:

src/cartStore.js
import { atom } from 'nanostores';
export const isCartOpen = atom(false);

Jetzt können wir diesen Store in jede Datei importieren, die lesen oder schreiben muss. Wir beginnen damit, unser CartFlyoutToggle zu verknüpfen:

src/components/CartFlyoutToggle.jsx
import { useStore } from '@nanostores/preact';
import { isCartOpen } from '../cartStore';
export default function CartButton() {
// lies den Speicherwert mit dem `useStore`-Hook
const $isCartOpen = useStore(isCartOpen);
// schreibe in den importierten Speicher mit `.set`
return (
<button onClick={() => isCartOpen.set(!$isCartOpen)}>Cart</button>
)
}

Dann können wir isCartOpen von unserer CartFlyout-Komponente lesen:

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

Jetzt wollen wir den Überblick über die Artikel in deinem Warenkorb behalten. Um Duplikate zu vermeiden und den Überblick über die “Menge” zu behalten, können wir deinen Warenkorb als Objekt mit der ID des Artikels als Schlüssel speichern. Wir verwenden dafür eine Map.

Jetzt fügen wir einen cartItem-Store zu unserer cartStore.js von vorhin hinzu. Du kannst auch zu einer TypeScript-Datei wechseln, um die Form zu definieren, wenn du das möchtest.

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

Jetzt exportieren wir einen addCartItem-Helper (Hilfsfunktion), den unsere Komponenten verwenden können.

  • Wenn dieser Artikel nicht in deinem Warenkorb ist, füge den Artikel mit einer Startmenge von 1 hinzu.
  • Wenn es diesen Artikel doch schon gibt, erhöhe die Menge um 1.
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 }
);
}
}

Wenn unser Shop eingerichtet ist, können wir diese Funktion in unserem AddToCartForm aufrufen, sobald das Formular abgeschickt wird. Außerdem öffnen wir das Warenkorb-Flyout, damit du eine vollständige Warenkorbübersicht siehst.

src/components/AddToCartForm.jsx
import { addCartItem, isCartOpen } from '../cartStore';
export default function AddToCartForm({ children }) {
// der Einfachheit halber codieren wir die Artikelinformationen fest!
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>
)
}

Zum Schluss stellen wir die Artikel im Warenkorb in unserem CartFlyout dar:

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>Dein Warenkorb ist leer!</p>}
</aside>
) : null;
}

Jetzt solltest du ein vollständig interaktives E-Commerce-Beispiel mit dem kleinsten JS-Bündel der Galaxis haben 🚀

Probiere das fertige Beispiel auf deinem Rechner oder online über Stackblitz aus!