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 .
Warum 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:
FAQ
🙋 Kann ich Nano Stores in .astro
-Dateien oder anderen serverseitigen Komponenten verwenden? Nano Stores können in serverseitigen Komponenten importiert, beschrieben und gelesen werden, aber wir empfehlen es nicht! Das liegt an einigen Einschränkungen:
Das Schreiben in einen Store aus einer .astro
-Datei oder einer nicht-hydratisierten Komponente hat keine Auswirkung auf den Wert, der von Client-seitigen Komponenten (EN) empfangen wird.
Du kannst einen Nano Store nicht als “prop” an clientseitige Komponenten übergeben.
Du kannst keine Änderungen aus einer Astro
-Datei abonnieren, da Astro-Komponenten nicht neu gerendert werden.
Wenn du diese Einschränkungen verstehst und trotzdem einen Anwendungsfall findest, kannst du Nano Stores ausprobieren! Denke daran, dass Nano Stores speziell für die Reaktivität auf Änderungen auf dem Client konzipiert sind.
🙋 Wie unterscheiden sich die Svelte Stores von den Nano Stores? Nano Stores und Svelte Stores sind sich sehr ähnlich! Tatsächlich kannst du in Nanostores die gleiche $
-Verknüpfung für Subscriptions verwenden, die du auch in Svelte Stores benutzen kannst.
Wenn du Bibliotheken von Drittanbietern vermeiden willst, sind Svelte Stores ein großartiges Werkzeug für die inselübergreifende Kommunikation. Dennoch könntest du Nano Stores bevorzugen, wenn dir a) die Add-ons für “objects” und async state gefallen oder b) du zwischen Svelte und anderen UI-Frameworks wie Preact oder Vue kommunizieren möchtest.
🙋 Wie unterscheiden sich Solid Signals von Nano Stores? Wenn du schon eine Weile mit Solid arbeitest, hast du vielleicht schon versucht, signals oder stores außerhalb deiner Komponenten zu verschieben. Dies ist eine großartige Möglichkeit, den Zustand zwischen Solid-Inseln zu teilen! Versuche, Signals aus einer gemeinsamen Datei zu exportieren:
import { createSignal } from ' solid-js ' ;
export const sharedCount = createSignal ( 0 );
…und alle Komponenten, die sharedCount
importieren, teilen sich denselben Zustand. Obwohl dies gut funktioniert, könntest du Nano Stores bevorzugen, wenn du a) die Add-ons für “objects” und async state magst, oder b) zwischen Solid und anderen UI-Frameworks wie Preact oder Vue kommunizieren möchtest.
Nano Stores installieren
Um loszulegen, installiere Nano Stores zusammen mit dem Hilfspaket für dein bevorzugtes UI-Framework:
npm install nanostores @nanostores/preact
npm install nanostores @nanostores/react
npm install nanostores @nanostores/solid
npm install nanostores @nanostores/vue
Du kannst die Nano-Stores-Anleitung von hier aus aufrufen oder unserem Beispiel unten folgen!
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:
---
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 >
“Atoms” verwenden
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” :
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:
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 >
)
}
import { useStore } from ' @nanostores/react ' ;
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 >
)
}
import { useStore } from ' @nanostores/solid ' ;
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 >
)
}
< script >
import { isCartOpen } from ' ../cartStore ' ;
</ script >
<!--verwende "$", um den Speicherwert zu lesen-->
< button on :click= { () => isCartOpen . set ( ! $ isCartOpen) } > Cart </ button >
< template >
<!--schreibe in den importierten Speicher mit `.set`-->
< button @click = " isCartOpen.set(!$isCartOpen) " > Cart </ button >
</ template >
< script setup >
import { isCartOpen } from ' ../cartStore ' ;
import { useStore } from ' @nanostores/vue ' ;
// lies den Speicherwert mit dem `useStore`-Hook
const $isCartOpen = useStore ( isCartOpen );
</ script >
Dann können wir isCartOpen
von unserer CartFlyout
-Komponente lesen:
import { useStore } from ' @nanostores/preact ' ;
import { isCartOpen } from ' ../cartStore ' ;
export default function CartFlyout () {
const $isCartOpen = useStore ( isCartOpen );
return $isCartOpen ? < aside > ... </ aside > : null ;
}
import { useStore } from ' @nanostores/react ' ;
import { isCartOpen } from ' ../cartStore ' ;
export default function CartFlyout () {
const $isCartOpen = useStore ( isCartOpen );
return $isCartOpen ? < aside > ... </ aside > : null ;
}
import { useStore } from ' @nanostores/solid ' ;
import { isCartOpen } from ' ../cartStore ' ;
export default function CartFlyout () {
const $isCartOpen = useStore ( isCartOpen );
return $isCartOpen () ? < aside > ... </ aside > : null ;
}
< script >
import { isCartOpen } from ' ../cartStore ' ;
</ script >
{# if $isCartOpen}
< aside > ... </ aside >
{/ if }
< template >
< aside v-if = " $isCartOpen " > ... </ aside >
</ template >
< script setup >
import { isCartOpen } from ' ../cartStore ' ;
import { useStore } from ' @nanostores/vue ' ;
const $isCartOpen = useStore ( isCartOpen );
</ script >
“Maps” verwenden
Tipp
Maps sind eine gute Wahl für Objekte, in die du regelmäßig schreibst! Neben den standardmäßigen get()
- und set()
-Hilfen, die ein atom
bietet, hast du auch eine .setKey()
-Funktion, um einzelne Objektschlüssel effizient zu aktualisieren.
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.
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 ( {} );
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 >> ( {} );
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.
...
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 }
);
}
}
...
type ItemDisplayInfo = Pick < CartItem , ' id ' | ' name ' | ' imageSrc ' >;
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 }
);
}
}
Hinweis
🙋 Warum wird hier .get()
anstelle eines useStore
-Helpers verwendet? Du hast vielleicht bemerkt, dass wir hier cartItems.get()
aufrufen, anstatt den useStore
-Helper aus unseren React / Preact / Solid / Vue-Beispielen zu verwenden. Das liegt daran, dass useStore dafür gedacht ist, die Komponenten neu zu rendern . Mit anderen Worten, useStore
sollte immer dann verwendet werden, wenn der Wert des Shops auf der Benutzeroberfläche dargestellt wird. Da wir den Wert auslesen, wenn ein Ereignis ausgelöst wird (in diesem Fall AddToCart
), und wir nicht versuchen, den Wert zu rendern, brauchen wir useStore
hier nicht.
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.
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 >
)
}
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 >
)
}
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 >
)
}
< form on :submit| preventDefault = { addToCart } >
< slot ></ slot >
</ form >
< script >
import { addCartItem, isCartOpen } from ' ../cartStore ' ;
// der Einfachheit halber codieren wir die Artikelinformationen fest!
const hardcodedItemInfo = {
id: ' astronaut-figurine ' ,
name: ' Astronaut Figurine ' ,
imageSrc: ' /images/astronaut-figurine.png ' ,
}
function addToCart () {
isCartOpen . set ( true );
addCartItem ( hardcodedItemInfo );
}
</ script >
< template >
< form @submit = " addToCart " >
< slot ></ slot >
</ form >
</ template >
< script setup >
import { addCartItem, isCartOpen } from ' ../cartStore ' ;
// 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 );
}
</ script >
Zum Schluss stellen wir die Artikel im Warenkorb in unserem CartFlyout
dar:
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 ;
}
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 > Dein Warenkorb ist leer! </ p > }
</ aside >
) : null ;
}
import { useStore } from ' @nanostores/solid ' ;
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 ;
}
< 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 > Dein Warenkorb ist leer! </ p >
{/ if }
{/ if }
< 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 > Menge: {{cartItem.quantity}} </ p >
</ li >
</ ul >
< p v-else > Dein Warenkorb ist leer! </ p >
</ aside >
</ template >
< script setup >
import { cartItems, isCartOpen } from ' ../cartStore ' ;
import { useStore } from ' @nanostores/vue ' ;
const $isCartOpen = useStore ( isCartOpen );
const $cartItems = useStore ( cartItems );
</ script >
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!
Learn