Astro 進階課程 2. 狀態管理與跨組件通訊 —— Nano Stores - 實作筆記

更新 發佈閱讀 17 分鐘

✏️ 專題二實作

1. 建立 Store (src/store/themeStore.ts)

import { atom } from 'nanostores'

// 預設為 false (淺色模式)
export const $isDark = atom(false)

2. 建立切換按鈕 (src/components/ThemeToggle.tsx)

import { useStore } from '@nanostores/react'
import { $isDark } from '../store/themeStore'

export default function ThemeToggle() {
const isDark = useStore($isDark)

return (
<button
onClick={() => $isDark.set(!isDark)}
className="p-2 rounded-lg bg-gray-200 dark:bg-slate-700 transition-colors"
>
{isDark ? '🌙 深色模式' : '☀️ 淺色模式'}
</button>
)
}

3. 在 Layout 中監聽並切換 Class (src/layouts/BaseLayout.astro)

這是最進階的部分。因為 <html> 標籤在 Astro 組件中,而 useStore是 React 的 Hook,我們不能直接在 Astro 的 Frontmatter 使用它來控制 HTML 屬性。

---
import ThemeToggle from '../components/ThemeToggle';
---

<html lang="zh-Hant">
<head>
<meta charset="utf-8" />
<title>NanoStore Dark Mode</title>
</head>
<body class="bg-white text-black dark:bg-slate-900 dark:text-white transition-colors duration-300">
<nav class="p-4 border-b">
<ThemeToggle client:load />
</nav>

<slot />

<script>
import { $isDark } from '../store/themeStore';

// 訂閱 Store 的變化
$isDark.subscribe((isDark) => {
if (isDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
});
</script>
</body>
</html>

4. 修改 src/styles/global.css (或你的主 CSS 檔)

在 Tailwind v4 中,要啟用手動切換深色模式,你必須在 CSS 中明確指定 variant

@import 'tailwindcss';

/* 關鍵設定:手動指定 dark 模式的觸發方式 */
@custom-variant dark (&:where(.dark, .dark *));

5. 測試效果

可以在任何頁面的元素上加上 dark: 前綴來測試:

<div class="p-10 bg-blue-500 dark:bg-red-500">
此區塊在淺色時是藍色,深色時會變成紅色。
</div>

📚 額外筆記

✅ Nanostores 是什麼

Nanostores 是一個專為現代前端開發設計的 狀態管理庫 (State Management Library)。它的核心理念是「極簡」、「原生支持多框架」以及「極小體積」。

如果你用過 Redux、Vuex 或 Pinia,Nanostores 提供的功能類似,但其哲學非常不同。

1. 核心特性

  • 極小體積 (Tiny Weight): 它的代碼量非常少(通常小於 1 KB),對應用的加載速度幾乎沒有影響。
  • 框架無關 (Framework Agnostic): 這是它最大的特色。你可以同一套狀態邏輯,同時用在 React、Vue、Svelte、Solid 甚至是原生 JavaScript 中。
  • 原子化 (Atomic State): 狀態被拆分成許多微小的「原子(Atoms)」,只有訂閱了該原子的組件才會在數據變化時重新渲染。
  • 樹搖優化 (Tree-shakable): 只會打包你實際用到的代碼,適合對性能要求極高的項目。

2. 為何選擇 Nanostores?

通常我們在開發大型應用時,狀態管理往往與特定的框架綁定。但隨著 Astro 這種多框架並存的方案流行,Nanostores 變得非常有用:

  • 跨框架通信: 假設你的頁面頂部導航欄是用 React 寫的,而側邊欄是用 Vue 寫的,你可以使用 Nanostores 讓這兩個組件共享同一個「購物車」或「用戶登入」狀態。
  • 代碼可移植性: 邏輯與 UI 框架分離,方便未來更換前端技術棧。

3. 基本運作邏輯

Nanostores 主要通過兩種核心對象來管理數據:

  1. atom: 用於管理單一的基礎數據(如數字、字符串)。
  2. map: 用於管理複雜的對象或字典。

代碼示例(以簡單計數器為例):

// store.js - 定義狀態
import { atom } from 'nanostores'

export const $counter = atom(0) // 加上 $ 是慣例,代表這是一個 store

export function increaseCounter() {
$counter.set($counter.get() + 1)
}
// React 組件中使用
import { useStore } from '@nanostores/react'
import { $counter, increaseCounter } from './store.js'

export const Counter = () => {
const count = useStore($counter) // 自動訂閱並更新
return <button onClick={increaseCounter}>{count}</button>
}

4. 總結

Nanostores 非常適合以下場景:

  • 使用 Astro 構建的多框架項目。
  • 對 Bundle Size(打包體積)有嚴苛要求的微型應用。
  • 希望將業務邏輯從 UI 框架中完全解耦。

如果你正在尋找一個比 Redux 輕量、比原生 API 更強大且能跨框架使用的方案,Nanostores 是一個極佳的選擇。


❓ const isDark = useStore($isDark)

在 Nanostores 的語境下,$isDark 是資料來源(Store),而 useStore 是 React 提供的一個掛鉤(Hook),用來監測這個來源。

核心運作邏輯

可以把這行程式碼拆解成三個層次來看:

  1. 監聽 (Listening): useStore 會告訴 React:「嘿,請幫我盯著 $isDark 這個原子。一旦它的值變了,請重新渲染(Re-render)這個組件。」
  2. 同步 (Syncing): 它會把 $isDark 目前內含的值(例如 false)提取出來,賦值給本地變數 isDark
  3. 自動化: 當組件被銷毀(例如使用者切換頁面)時,useStore 會自動取消訂閱,防止記憶體洩漏。

✅ 關於 @nanostores/react

1. 為什麼 React 官方沒有 useStore

React 官方提供的工具(Hooks)通常是以 use... 開頭,例如:

  • useState: 用來管理組件內部的私有狀態。
  • useEffect: 用來處理副作用。
  • useContext: 用來讀取 React 自身的 Context。

React 並不知道 Nanostores 的存在。Nanostores 是一個獨立於框架之外的狀態管理庫(它甚至可以用在 Vue, Svelte 或純 JS 中)。

2. @nanostores/react 的角色:翻譯官

@nanostores/react 是一個專門為 React 寫的「適配器(Adapter)」。

它的作用是將 Nanostores 的原始資料轉換成 React 能理解的狀態。當你呼叫 useStore($isDark) 時,它底層其實幫你做了這幾件事:

  1. 在組件掛載時,手動訂閱 $isDark
  2. 當 $isDark 改變時,觸發 React 內部的 setState 讓畫面更新。
  3. 在組件卸載時,自動取消訂閱。

3. 對比表格:區分不同來源的 Hook

raw-image

4. 總結你的程式碼邏輯

在你的任務中:
你的資料(Store)放在 themeStore.ts
你的組件(React)需要讀取它。
結論:你必須使用 @nanostores/react 提供的 useStore 作為兩者之間的橋樑。

💡 小提醒: 如果你在 Astro 專案中同時使用了 Vue 組件,你會發現你需要 import { useStore } from '@nanostores/vue'。這就是 Nanostores 強大的地方——同一個 Store,不同的橋樑。


✅ Atom

  • import { Atom } from "nanostores";
  • import { atom } from "nanostores";

在 nanostores 的庫中,這兩者的區別在於 「執行邏輯」 與 「類型定義」 的不同。
簡單來說:atom 是你拿來「用」的工具,而 Atom 是你拿來「看」的規格

1. atom (小寫):建立 Store 的函數

這是一個 JavaScript 函數。當你想創建一個新的狀態(State)時,你會呼叫它。

  • 用途:初始化一個存儲數據的實體。
  • 特性:它會返回一個包含 .get().set().subscribe() 等方法的物件。

1.1 常用的三大方法

  • .set(newValue): 直接覆蓋 Store 的值。
  • .subscribe(callback): 最重要的方法。它會在你訂閱的那一刻「立即執行一次」,之後每當值變動時都會再次執行。它會返回一個 unsub函數,呼叫它就可以停止監聽。
  • .get(): 當你只需要「現在這一秒」的值,而不需要後續監聽時使用。例如:在一個 Function 裡判斷 if ($isDark.get()) { ... }

1.2 進階的「生命週期」方法

這部分通常是進階開發者用來優化效能的,隱藏在 onMount 等工具函數中:

  • onMount: 這不是 Atom 的方法,但它是配合 Atom 使用的。你可以定義當「有人開始聽我」和「所有人都停止聽我」時要做什麼。
    • 應用場景:當有人訂閱時,才開始建立 WebSocket 連線;沒人聽時就斷開連線,節省效能。

1.3 與 Map Store 的區別

如果你使用的是 atom,它就像一個單一儲存格。 但如果你改用 map(另一種 Nanostores 類型),它會有更多方法:

  • .setKey(key, value):只修改物件中的某個屬性,而不是覆蓋整個物件。

2. 為什麼要有 .get() 而不直接用變數?

這涉及到 JavaScript 的引用機制。 如果 $isDark 只是個普通變數,一旦它被導入到其他檔案,它就死掉了(無法追蹤更新)。 包裝成 Atom 後,它變成了一個物件。當你執行 $isDark.get(),你是在向這個物件「請求」它肚子裡的最新資料。

import { atom } from 'nanostores'

// 建立一個初始值為 0 的 store
export const $counter = atom(0)

// 操作 store
$counter.set(10)
console.log($counter.get()) // 輸出 10

2. Atom (大寫):TypeScript 的類型 (Type)

這是一個 TypeScript 接口(Interface)。它在程式執行時不存在,只在開發階段用來標記型別。

  • 用途:當你寫一個函數,而這個函數需要接收一個 store 作為參數時,用來告訴編輯器這個參數必須符合 Atom 的規格。
  • 特性Atom<T> 通常代表「唯讀」的規格(只有 .get 和 .subscribe)。如果你需要代表「可寫入」的規格,會使用 WritableAtom
  • WritableAtom<T>:代表一個「可寫入」的 Store 接口(有 .set() 和 .setKey())。
import { Atom, atom } from 'nanostores'

const $status = atom('loading')

// 使用 Atom 作為參數類型,確保傳進來的是一個 nanostores 實體
function checkStatus(store: Atom<string>) {
console.log('當前狀態是:' + store.get())
}

checkStatus($status)

🧠 專題二 - 運作流程

1. themeStore.ts:資料的中心

它不屬於任何一個頁面,它是一個獨立的 Atom (原子)。它的作用只有一個:守護 isDark 這個變數。

  • 它提供 set() 方法讓別人修改值。
  • 它提供 subscribe() 方法讓別人監聽變化。

2. ThemeToggle.tsx:使用者互動

這是一個 React 組件

  • 它讀取值:透過 useStore($isDark)。當 Store 變動時,React 會發現 isDark 變了,於是按鈕上的文字會從「🌙」變成「☀️」。

-- 它修改值:當你點擊 onClick 時,它執行 $isDark.set(!isDark)。這一行程式碼會把新訊息傳回給 themeStore.ts

3. BaseLayout.astro:全局控制

這是最關鍵的一步。雖然按鈕變了,但如果我們不操作 DOM,網頁背景還是不會變。

  • 為什麼要寫在 <script> 裡? 因為 Astro 的 HTML 渲染完後就「固定」了。為了讓網頁能實時變色,我們需要一段 JavaScript 在瀏覽器裡跑。
  • 運作機制: 這段腳本會向 themeStore.ts 訂閱(Subscribe)。

完成 進階課程 2 ,照理說下一篇要是 專題三:持久化資料存放 —— 整合 Supabase 或 Drizzle ORM ,但還是對 課程2 懵懵懂懂,所以接下來幾小篇會再多練習一下 👍

留言
avatar-img
李昀瑾的沙龍
0會員
32內容數
李昀瑾的沙龍的其他內容
2026/01/20
Nano Stores與其它的狀態管理工具(如 Redux 或 Pinia)不同 Nano Stores 是 不可知框架(Framework-agnostic) 的。這意味著你可以在同一個 Astro 專案中,讓 React、Vue、Svelte 和原生 JS 共享同一個狀態。 第一階段:核心概
2026/01/20
Nano Stores與其它的狀態管理工具(如 Redux 或 Pinia)不同 Nano Stores 是 不可知框架(Framework-agnostic) 的。這意味著你可以在同一個 Astro 專案中,讓 React、Vue、Svelte 和原生 JS 共享同一個狀態。 第一階段:核心概
2026/01/17
專題二:Nano Stores —— 連結孤島的橋樑 為什麼需要它? 在 Astro 中,每個互動組件(React, Vue, Svelte)都是一個獨立的「孤島 (Island)」。 問題:如果你在 Header.tsx (React) 有一個購物車圖示,在 ProductCard.tsx 
2026/01/17
專題二:Nano Stores —— 連結孤島的橋樑 為什麼需要它? 在 Astro 中,每個互動組件(React, Vue, Svelte)都是一個獨立的「孤島 (Island)」。 問題:如果你在 Header.tsx (React) 有一個購物車圖示,在 ProductCard.tsx 
2026/01/15
🚀 專題一:Tailwind CSS 實戰開始 我們先從最能直接提升成就感的 Tailwind CSS 開始。 1. 安裝 Tailwind 在你的專案目錄執行: npx astro add tailwind (全部選 Yes,這會自動設定 astro.config.mjs 並引入指令
2026/01/15
🚀 專題一:Tailwind CSS 實戰開始 我們先從最能直接提升成就感的 Tailwind CSS 開始。 1. 安裝 Tailwind 在你的專案目錄執行: npx astro add tailwind (全部選 Yes,這會自動設定 astro.config.mjs 並引入指令
看更多
你可能也想看
Thumbnail
債券投資,不只是高資產族群的遊戲 在傳統的投資觀念中,海外債券(Overseas Bonds)常被貼上「高資產族群專屬」的標籤。過去動輒 1 萬甚至 10 萬美元的最低申購門檻,讓許多想尋求穩定配息的小資族望而卻步。 然而,在股市波動劇烈的環境下,尋求穩定的美元現金流與被動收入成為許多投資人
Thumbnail
債券投資,不只是高資產族群的遊戲 在傳統的投資觀念中,海外債券(Overseas Bonds)常被貼上「高資產族群專屬」的標籤。過去動輒 1 萬甚至 10 萬美元的最低申購門檻,讓許多想尋求穩定配息的小資族望而卻步。 然而,在股市波動劇烈的環境下,尋求穩定的美元現金流與被動收入成為許多投資人
Thumbnail
透過川普的近期債券交易揭露,探討債券作為資產配置中「穩定磐石」的重要性。文章分析降息對債券的潛在影響,以及股神巴菲特的操作策略。並介紹玉山證券「小額債」平臺,如何讓小資族也能低門檻參與海外債券市場,實現「低門檻、低波動、固定收益」的務實投資方式。
Thumbnail
透過川普的近期債券交易揭露,探討債券作為資產配置中「穩定磐石」的重要性。文章分析降息對債券的潛在影響,以及股神巴菲特的操作策略。並介紹玉山證券「小額債」平臺,如何讓小資族也能低門檻參與海外債券市場,實現「低門檻、低波動、固定收益」的務實投資方式。
Thumbnail
解析「債券」如何成為資產配置中的穩定錨,提供低風險高回報的投資選項。 藉由玉山證券的低門檻債券服務,投資者可輕鬆入手,平衡風險並穩定財務。
Thumbnail
解析「債券」如何成為資產配置中的穩定錨,提供低風險高回報的投資選項。 藉由玉山證券的低門檻債券服務,投資者可輕鬆入手,平衡風險並穩定財務。
Thumbnail
相較於波動較大的股票,債券能提供固定現金流,而玉山證券推出的小額債,更以1000 美元的低門檻,讓學生與新手也能參與全球優質企業債投資。玉山E-Trader平台即時報價、條件式篩選與清楚的交易流程等特色,大幅降低投資難度,對於希望分散風險、建立穩定現金流的人來說,玉山小額債是一個值得嘗試的理財起點。
Thumbnail
相較於波動較大的股票,債券能提供固定現金流,而玉山證券推出的小額債,更以1000 美元的低門檻,讓學生與新手也能參與全球優質企業債投資。玉山E-Trader平台即時報價、條件式篩選與清楚的交易流程等特色,大幅降低投資難度,對於希望分散風險、建立穩定現金流的人來說,玉山小額債是一個值得嘗試的理財起點。
Thumbnail
這是一場從「網路連結 → 線下見面」的活動,我一開始其實有些猶豫,畢竟地點對我來說不近,加上平常在社群裡其實不太主動互動。 但因為主辦人西打誠意滿滿地邀請,甚至還提出補貼車資,最後我決定自費參加。現在回頭看,真的很值得! 場地很有感,氛圍超溫暖 一踏進場地就被暖黃的燈光包圍,小閣樓超舒適,還
Thumbnail
這是一場從「網路連結 → 線下見面」的活動,我一開始其實有些猶豫,畢竟地點對我來說不近,加上平常在社群裡其實不太主動互動。 但因為主辦人西打誠意滿滿地邀請,甚至還提出補貼車資,最後我決定自費參加。現在回頭看,真的很值得! 場地很有感,氛圍超溫暖 一踏進場地就被暖黃的燈光包圍,小閣樓超舒適,還
Thumbnail
從實際應用中學習 Python 程式設計,提升技能並建立作品集。文章提供八個循序漸進的 Python 專案範例,涵蓋檔案操作、網路爬蟲、Web 應用、自動化腳本、數據分析、遊戲開發、API 互動及應用程式部署,並附上實戰建議及學習資源。
Thumbnail
從實際應用中學習 Python 程式設計,提升技能並建立作品集。文章提供八個循序漸進的 Python 專案範例,涵蓋檔案操作、網路爬蟲、Web 應用、自動化腳本、數據分析、遊戲開發、API 互動及應用程式部署,並附上實戰建議及學習資源。
Thumbnail
網站開發專案成功的關鍵在於與客戶的有效溝通。本文分享一個成功案例,說明如何透過明確掌握專案需求、主動提供技術方案、定期回報進度、完善技術協助及建立良好客戶關係,順利完成一個中文影片學習分享網站的建置,並獲得客戶高度滿意與後續合作機會。
Thumbnail
網站開發專案成功的關鍵在於與客戶的有效溝通。本文分享一個成功案例,說明如何透過明確掌握專案需求、主動提供技術方案、定期回報進度、完善技術協助及建立良好客戶關係,順利完成一個中文影片學習分享網站的建置,並獲得客戶高度滿意與後續合作機會。
Thumbnail
Nuxt.js 是以 Vue 為基底所建構的框架,透過 Nuxt.js,我們能夠更輕鬆地開發靜態頁面 (Static Site)、操作體驗良好的單頁式網站 (SPA)、甚至是顧及 SEO 的伺服器端渲染 (SSR) 網站。
Thumbnail
Nuxt.js 是以 Vue 為基底所建構的框架,透過 Nuxt.js,我們能夠更輕鬆地開發靜態頁面 (Static Site)、操作體驗良好的單頁式網站 (SPA)、甚至是顧及 SEO 的伺服器端渲染 (SSR) 網站。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News