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 组件使用 useState 和 useEffect 管理状态:
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 组件使用 ref 和 computed 管理状态:
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 的框架无关状态管理库,为每个框架提供适配器,实现跨岛屿状态同步。
安装
bashnpm 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 项目全局状态 | Zustand | API 简洁,社区活跃 |
| API 数据缓存 | TanStack Query | 自动缓存和失效更新 |
| 筛选/分页/排序 | URL 参数 | 可分享,支持前进后退 |
| 静态内容展示 | Astro frontmatter 直接获取 | 零 JS 开销 |
选择状态管理方案时,核心判断依据是状态的共享范围:岛屿内部用框架内置方案,跨岛屿用 Nanostores,服务端数据优先在 Astro frontmatter 中直接获取。