5月27日 15:55

Astro 中的状态管理是如何实现的?如何在 React、Vue、Svelte 组件中管理状态?

Astro 的状态管理与传统 SPA 框架有本质区别。理解 Astro 的岛屿架构(Island Architecture)是掌握其状态管理的关键前提。

岛屿架构如何影响状态管理

Astro 默认发送零 JavaScript,页面以纯静态 HTML 输出。交互组件需要通过 client:* 指令才会被水合(hydrate)并获得客户端状态管理能力。这意味着:

  • 没有 client:* 指令的组件只渲染静态 HTML,点击按钮不会触发状态更新
  • 每个岛屿(island)是独立的,拥有各自的生命周期和状态作用域
  • 岛屿之间默认无法共享状态,这是 Astro 状态管理的核心挑战

Astro 提供五种水合指令控制组件何时激活:

  • client:load:页面加载后立即水合,适合首屏关键交互
  • client:idle:浏览器空闲时水合,适合低优先级组件
  • client:visible:组件进入视口时水合,适合首屏以下内容
  • client:media:匹配媒体查询时水合,适合响应式场景
  • client:only:跳过服务端渲染,仅在客户端渲染

各框架组件的局部状态管理

加上 client:* 指令后,各框架组件使用各自的内置状态机制,写法与在原生框架中完全一致。

React 组件

React 组件使用 useStateuseEffect 管理状态:

jsx
// src/components/Counter.jsx import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); return ( <div> <p>当前计数: {count}</p> <button onClick={() => setCount(c => c + 1)}>+1</button> <button onClick={() => setCount(c => c - 1)}>-1</button> </div> ); }

在 Astro 页面中使用时必须加上 client:* 指令:

astro
--- import Counter from '../components/Counter.jsx'; --- <Counter client:load />

如果漏掉 client:load,组件只会渲染初始 HTML,按钮点击不会有任何响应。

Vue 组件

Vue 组件使用 refcomputed 管理状态:

vue
<!-- src/components/TodoList.vue --> <script setup> import { ref, computed } from 'vue'; const todos = ref([ { id: 1, text: '学习 Astro', done: false }, { id: 2, text: '构建应用', done: true }, ]); const remaining = computed(() => todos.value.filter(t => !t.done).length ); function toggle(id) { const todo = todos.value.find(t => t.id === id); if (todo) todo.done = !todo.done; } </script> <template> <p>剩余 {{ remaining }} 项</p> <ul> <li v-for="todo in todos" :key="todo.id"> <input type="checkbox" :checked="todo.done" @change="toggle(todo.id)" /> <span :style="{ textDecoration: todo.done ? 'line-through' : 'none' }"> {{ todo.text }} </span> </li> </ul> </template>

在 Astro 页面中使用:

astro
--- import TodoList from '../components/TodoList.vue'; --- <TodoList client:visible />

Svelte 组件

Svelte 使用内置 store 管理状态,语法最简洁:

svelte
<!-- src/components/Cart.svelte --> <script> import { writable } from 'svelte/store'; const items = writable([]); const total = writable(0); function add(product) { items.update(list => [...list, product]); total.update(t => t + product.price); } </script> <h2>购物车</h2> <p>合计: {$total} 元</p> {#each $items as item} <p>{item.name} - {item.price} 元</p> {/each}

跨框架状态共享:Nanostores

各框架的局部状态(React 的 useState、Vue 的 ref、Svelte 的 writable)只能在同一个岛屿内部使用。当不同框架的岛屿需要共享状态时,Astro 官方推荐使用 Nanostores。

Nanostores 是一个不到 1KB 的框架无关状态管理库,为每个框架提供适配器,实现跨岛屿状态同步。

安装

bash
npm install nanostores @nanostores/react @nanostores/vue # Svelte 无需额外包,原生支持

创建共享 Store

js
// src/store/cartStore.js import { atom, map } from 'nanostores'; export const isCartOpen = atom(false); export const cartItems = map({}); export function addCartItem(name, price) { cartItems.set({ ...cartItems.get(), [name]: { name, price } }); isCartOpen.set(true); }

React 岛屿读取共享状态

jsx
// src/components/CartButton.jsx import { useStore } from '@nanostores/react'; import { isCartOpen, cartItems } from '../store/cartStore'; export default function CartButton() { const $isOpen = useStore(isCartOpen); const $items = useStore(cartItems); const count = Object.keys($items).length; return ( <button onClick={() => isCartOpen.set(!$isOpen)}> 购物车 ({count}) </button> ); }

Vue 岛屿读取共享状态

vue
<!-- src/components/CartPanel.vue --> <script setup> import { useStore } from '@nanostores/vue'; import { isCartOpen, cartItems } from '../store/cartStore'; const $isOpen = useStore(isCartOpen); const $items = useStore(cartItems); </script> <template> <div v-if="$isOpen" class="cart-panel"> <h3>购物车</h3> <ul> <li v-for="(item, key) in $items" :key="key"> {{ item.name }} - {{ item.price }} 元 </li> </ul> </div> </template>

Svelte 岛屿读取共享状态

svelte
<!-- src/components/CartCount.svelte --> <script> import { isCartOpen, cartItems } from '../store/cartStore'; </script> <span>商品数: {Object.keys($cartItems).length}</span> <button on:click={() => isCartOpen.set(!$isCartOpen)}> {#if $isCartOpen}关闭{:else}打开{/if} </button>

在 Astro 页面中组合

astro
--- import CartButton from '../components/CartButton.jsx'; import CartPanel from '../components/CartPanel.vue'; import CartCount from '../components/CartCount.svelte'; --- <header> <CartButton client:load /> <CartCount client:load /> </header> <main> <CartPanel client:load /> </main>

三个不同框架的岛屿通过 Nanostores 共享同一份购物车状态,修改一个岛屿中的数据会实时反映到其他岛屿。

全局状态管理方案选择

除了 Nanostores,Astro 项目还可以根据场景选择其他方案:

Zustand(React 专属项目)

如果项目只使用 React,Zustand 是更熟悉的选择:

ts
// src/store/useStore.ts import { create } from 'zustand'; interface AppState { user: { name: string } | null; theme: 'light' | 'dark'; setUser: (user: { name: string }) => void; setTheme: (theme: 'light' | 'dark') => void; } export const useStore = create<AppState>((set) => ({ user: null, theme: 'light', setUser: (user) => set({ user }), setTheme: (theme) => set({ theme }), }));

Zustand 的状态只在 React 岛屿内有效,无法直接被 Vue 或 Svelte 岛屿访问。

React Query / TanStack Query(服务端数据缓存)

用于管理从 API 获取的服务端状态,自动处理缓存、重试和失效更新:

jsx
// src/components/PostList.jsx import { useQuery } from '@tanstack/react-query'; export default function PostList() { const { data, isLoading, error } = useQuery({ queryKey: ['posts'], queryFn: () => fetch('/api/posts').then(r => r.json()), }); if (isLoading) return <p>加载中...</p>; if (error) return <p>加载失败</p>; return ( <ul> {data.map(post => <li key={post.id}>{post.title}</li>)} </ul> ); }

服务端数据获取

Astro 组件在服务端执行,可以直接在 frontmatter 中获取数据,无需客户端请求:

astro
--- // src/pages/dashboard.astro const user = await getUser(Astro.request); const posts = await getCollection('blog'); --- <h1>{user.name} 的仪表板</h1> <ul> {posts.map(post => <li>{post.data.title}</li>)} </ul>

这种方式获取的数据作为静态 HTML 输出,不产生客户端 JavaScript 开销,是 Astro 性能优势的核心来源。

URL 作为状态源

筛选、分页、排序等状态适合存储在 URL 参数中,好处是可分享、可前进后退:

jsx
// src/components/ProductFilter.jsx import { useSearchParams } from 'react-router-dom'; export default function ProductFilter() { const [params, setParams] = useSearchParams(); const category = params.get('category') || 'all'; const page = parseInt(params.get('page') || '1'); function update(key, value) { setParams(prev => { const next = new URLSearchParams(prev); next.set(key, value); return next; }); } return ( <div> <select value={category} onChange={e => update('category', e.target.value)}> <option value="all">全部分类</option> <option value="electronics">电子产品</option> </select> <button disabled={page === 1} onClick={() => update('page', page - 1)}>上一页</button> <span>{page}</span> <button onClick={() => update('page', page + 1)}>下一页</button> </div> ); }

状态管理方案选型指南

场景推荐方案说明
单岛屿内部状态框架内置(useState/ref/writable)最简单,无额外依赖
跨岛屿/跨框架共享Nanostores官方推荐,轻量通用
仅 React 项目全局状态ZustandAPI 简洁,社区活跃
API 数据缓存TanStack Query自动缓存和失效更新
筛选/分页/排序URL 参数可分享,支持前进后退
静态内容展示Astro frontmatter 直接获取零 JS 开销

选择状态管理方案时,核心判断依据是状态的共享范围:岛屿内部用框架内置方案,跨岛屿用 Nanostores,服务端数据优先在 Astro frontmatter 中直接获取。

标签:Astro