在使用 群岛架构/部分水化 搭建 Astro 网站时,你可能遇到过这样的问题:我想在我的组件之间共享状态。
¥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.
像 React 或 Vue 这样的 UI 框架可能会鼓励其他组件使用 “context” 提供商 。但是当 部分混合组件 在 Astro 或 Markdown 中时,你不能使用这些上下文封装器。
¥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 建议采用不同的共享客户端存储解决方案:纳米存储 。
¥Astro recommends a different solution for shared client-side storage: Nano Stores .
¥Why Nano Stores?
纳米存储 库允许你创作任何组件都可以与之交互的存储。我们推荐 Nano Store 的原因是:
¥The Nano Stores library allows you to author stores that any component can interact with. We recommend Nano Stores because:
尽管如此,你仍然可以探索许多替代方案。这些包括:
¥Still, there are a number of alternatives you can explore. These include:
FAQ
🙋 Can I use Nano Stores in .astro
files or other server-side components? Nano Stores can be used in <script>
tags to share state between .astro
components . However, Using Nano Stores in the frontmatter of server-side components is not recommended because of the following 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:
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
首先,为你最喜欢的 UI 框架安装 Nano Stores 及其辅助程序包:
¥To get started, install Nano Stores alongside their helper package for your favorite UI framework:
npm install nanostores @nanostores/preact
npm install nanostores @nanostores/react
npm install nanostores @nanostores/solid
npm install nanostores @nanostores/vue
npm install nanostores @nanostores/lit
你可以从这里跳入 Nano 存储使用指南 ,或者按照我们下面的示例进行操作!
¥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:
“添加到购物车” 提交表格
用于显示这些添加的项目的购物车弹出窗口
购物车弹出开关
尝试完成的示例 在你的计算机上或通过 StackBlitz 在线。
¥*Try the completed example on your machine or online via StackBlitz.*
你的基本 Astro 文件可能如下所示:
¥Your base Astro file may look like this:
import CartFlyoutToggle from ' ../components/CartFlyoutToggle ' ;
import CartFlyout from ' ../components/CartFlyout ' ;
import AddToCartForm from ' ../components/AddToCartForm ' ;
< a href = " / " > Astro storefront </ a >
< CartFlyoutToggle client:load />
< AddToCartForm client:load >
< CartFlyout client:load />
¥Using “atoms”
让我们首先在单击 CartFlyoutToggle
时打开 CartFlyout
。
¥Let’s start by opening our CartFlyout
whenever CartFlyoutToggle
is clicked.
首先,创建一个新的 JS 或 TS 文件来包含我们的存储。为此,我们将使用 “atom” :
¥First, create a new JS or TS file to contain our store. We’ll use an “atom” for this:
import { atom } from ' nanostores ' ;
export const isCartOpen = atom ( false );
现在,我们可以将此存储导入到任何需要读取或写入的文件中。我们首先连接 CartFlyoutToggle
:
¥Now, we can import this store into any file that needs to read or write. We’ll start by wiring up our CartFlyoutToggle
:
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`
< button onClick = { () => isCartOpen . set ( ! $isCartOpen ) } > Cart </ button >
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`
< button onClick = { () => isCartOpen . set ( ! $isCartOpen ) } > Cart </ button >
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`
< button onClick = { () => isCartOpen . set ( ! $isCartOpen ()) } > Cart </ button >
import { isCartOpen } from ' ../cartStore ' ;
<!--use "$" to read the store value-->
< button on :click= { () => isCartOpen . set ( ! $ isCartOpen) } > Cart </ button >
<!--write to the imported store using `.set`-->
< button @ click = " isCartOpen . set ( ! $isCartOpen) " > Cart </ button >
import { isCartOpen } from ' ../cartStore ' ;
import { useStore } from ' @nanostores/vue ' ;
// read the store value with the `useStore` hook
const $isCartOpen = useStore ( isCartOpen );
import { LitElement, html } from ' lit ' ;
import { isCartOpen } from ' ../cartStore ' ;
export class CartFlyoutToggle extends LitElement {
isCartOpen . set ( ! isCartOpen . get ());
< button @click = " ${this.handleClick} " > Cart </ button >
customElements . define ( ' cart-flyout-toggle ' , CartFlyoutToggle);
然后,我们可以从 CartFlyout
组件中读取 isCartOpen
:
¥Then, we can read isCartOpen
from our CartFlyout
component:
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 ;
import { isCartOpen } from ' ../cartStore ' ;
< aside v-if = " $isCartOpen " > ... </ aside >
import { isCartOpen } from ' ../cartStore ' ;
import { useStore } from ' @nanostores/vue ' ;
const $isCartOpen = useStore ( isCartOpen );
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);
return this . cartOpen . value ? html ` < aside > ... </ aside > ` : null ;
customElements . define ( ' cart-flyout ' , CartFlyout);
¥Using “maps”
现在,让我们跟踪购物车内的商品。为了避免重复并跟踪 “数量,“,我们可以将你的购物车存储为一个对象,并以商品 ID 作为键。为此,我们将使用 地图 。
¥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.
让我们将 cartItem
存储添加到之前的 cartStore.js
中。如果你愿意,还可以切换到 TypeScript 文件来定义形状。
¥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.
import { atom, map } from ' nanostores ' ;
export const isCartOpen = atom ( false );
* @typedef {Object} CartItem
* @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 const cartItems = map < Record < string , CartItem >> ( {} );
现在,让我们导出一个 addCartItem
辅助程序供我们的组件使用。
¥Now, let’s export an addCartItem
helper for our components to use.
export function addCartItem ( { id , name , imageSrc } ) {
const existingEntry = cartItems . get ()[ id ];
quantity: existingEntry . quantity + 1 ,
{ id , name , imageSrc , quantity: 1 }
type ItemDisplayInfo = Pick < CartItem , ' id ' | ' name ' | ' imageSrc ' >;
export function addCartItem ( { id , name , imageSrc } : ItemDisplayInfo ) {
const existingEntry = cartItems . get ()[id];
quantity: existingEntry . quantity + 1 ,
{ 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.
存储就位后,每当提交表单时,我们就可以在 AddToCartForm
内调用此函数。我们还将打开购物车弹出窗口,以便你可以看到完整的购物车摘要。
¥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.
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 ' ,
addCartItem ( hardcodedItemInfo );
< form onSubmit = { addToCart } >
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 ' ,
addCartItem ( hardcodedItemInfo );
< form onSubmit = { addToCart } >
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 ' ,
addCartItem ( hardcodedItemInfo );
< form onSubmit = { addToCart } >
< form on :submit| preventDefault = { addToCart } >
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 ' ,
addCartItem ( hardcodedItemInfo );
< form @ submit = " addToCart " >
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 ' ,
addCartItem ( hardcodedItemInfo );
import { LitElement, html } from ' lit ' ;
import { isCartOpen, addCartItem } from ' ../cartStore ' ;
export class AddToCartForm extends LitElement {
static get properties () {
< form @submit = " ${this.addToCart} " >
customElements . define ( ' add-to-cart-form ' , AddToCartForm);
最后,我们将在 CartFlyout
中渲染这些购物车项目:
¥Finally, we’ll render those cart items inside our CartFlyout
:
import { useStore } from ' @nanostores/preact ' ;
import { isCartOpen, cartItems } from ' ../cartStore ' ;
export default function CartFlyout () {
const $isCartOpen = useStore ( isCartOpen );
const $cartItems = useStore ( cartItems );
{ Object . values ( $cartItems ) . length ? (
{ Object . values ( $cartItems ) . map ( cartItem => (
< img src = { cartItem . imageSrc } alt = { cartItem . name } />
< p > Quantity: { cartItem . quantity } </ p >
) : < p > Your cart is empty! </ p > }
import { useStore } from ' @nanostores/react ' ;
import { isCartOpen, cartItems } from ' ../cartStore ' ;
export default function CartFlyout () {
const $isCartOpen = useStore ( isCartOpen );
const $cartItems = useStore ( cartItems );
{ Object . values ( $cartItems ) . length ? (
{ Object . values ( $cartItems ) . map ( cartItem => (
< img src = { cartItem . imageSrc } alt = { cartItem . name } />
< p > Quantity: { cartItem . quantity } </ p >
) : < p > Your cart is empty! </ p > }
import { useStore } from ' @nanostores/solid ' ;
import { isCartOpen, cartItems } from ' ../cartStore ' ;
export default function CartFlyout () {
const $isCartOpen = useStore ( isCartOpen );
const $cartItems = useStore ( cartItems );
{ Object . values ( $cartItems ()) . length ? (
{ Object . values ( $cartItems ()) . map ( cartItem => (
< img src = { cartItem . imageSrc } alt = { cartItem . name } />
< p > Quantity: { cartItem . quantity } </ p >
) : < p > Your cart is empty! </ p > }
import { isCartOpen, cartItems } from ' ../cartStore ' ;
{# if Object . values ($cartItems) . length }
{# each Object . values ($cartItems) as cartItem}
< img src = { cartItem . imageSrc } alt = { cartItem . name } />
< p > Quantity: { cartItem . quantity } </ p >
< p > Your cart is empty! </ p >
< 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 >
< p v-else > Your cart is empty! </ p >
import { cartItems, isCartOpen } from ' ../cartStore ' ;
import { useStore } from ' @nanostores/vue ' ;
const $isCartOpen = useStore ( isCartOpen );
const $cartItems = useStore ( cartItems );
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 ) {
< img src = " ${cartItem.imageSrc} " alt = " ${cartItem.name} " />
< h3 > ${ cartItem . name } </ h3 >
< p > Quantity: ${ cartItem . quantity } </ p >
return this . cartOpen . value
Object . values ( this . getCartItems . value ) . length
${ Object . values ( this . getCartItems . value ) . map ( ( cartItem ) =>
this . renderCartItem ( cartItem )
: html ` < p > Your cart is empty! </ p > `
customElements . define ( ' cart-flyout ' , CartFlyoutLit);
现在,你应该有一个完全交互式的电子商务示例,其中包含银河系中最小的 JS 包 🚀
¥Now, you should have a fully interactive ecommerce example with the smallest JS bundle in the galaxy 🚀
尝试完成的示例 在你的机器上或通过 StackBlitz 在线!
¥Try the completed example on your machine or online via StackBlitz!
Recipes