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

vocus|新世代的創作平台

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會員
36內容數
李昀瑾的沙龍的其他內容
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
這是一場修復文化與重建精神的儀式,觀眾不需要完全看懂《遊林驚夢:巧遇Hagay》,但你能感受心與土地團聚的渴望,也不急著在此處釐清或定義什麼,但你的在場感受,就是一條線索,關於如何找著自己的路徑、自己的聲音。
Thumbnail
這是一場修復文化與重建精神的儀式,觀眾不需要完全看懂《遊林驚夢:巧遇Hagay》,但你能感受心與土地團聚的渴望,也不急著在此處釐清或定義什麼,但你的在場感受,就是一條線索,關於如何找著自己的路徑、自己的聲音。
Thumbnail
Nuxt.js 是以 Vue 為基底所建構的框架,透過 Nuxt.js,我們能夠更輕鬆地開發靜態頁面 (Static Site)、操作體驗良好的單頁式網站 (SPA)、甚至是顧及 SEO 的伺服器端渲染 (SSR) 網站。
Thumbnail
Nuxt.js 是以 Vue 為基底所建構的框架,透過 Nuxt.js,我們能夠更輕鬆地開發靜態頁面 (Static Site)、操作體驗良好的單頁式網站 (SPA)、甚至是顧及 SEO 的伺服器端渲染 (SSR) 網站。
Thumbnail
從實際應用中學習 Python 程式設計,提升技能並建立作品集。文章提供八個循序漸進的 Python 專案範例,涵蓋檔案操作、網路爬蟲、Web 應用、自動化腳本、數據分析、遊戲開發、API 互動及應用程式部署,並附上實戰建議及學習資源。
Thumbnail
從實際應用中學習 Python 程式設計,提升技能並建立作品集。文章提供八個循序漸進的 Python 專案範例,涵蓋檔案操作、網路爬蟲、Web 應用、自動化腳本、數據分析、遊戲開發、API 互動及應用程式部署,並附上實戰建議及學習資源。
Thumbnail
5 月將於臺北表演藝術中心映演的「2026 北藝嚴選」《海妲・蓋柏樂》,由臺灣劇團「晃晃跨幅町」製作,本文將以從舞台符號、聲音與表演調度切入,討論海妲・蓋柏樂在父權社會結構下的困境,並結合榮格心理學與馮.法蘭茲對「阿尼姆斯」與「永恆少年」原型的分析,理解女人何以走向精神性的操控、毀滅與死亡。
Thumbnail
5 月將於臺北表演藝術中心映演的「2026 北藝嚴選」《海妲・蓋柏樂》,由臺灣劇團「晃晃跨幅町」製作,本文將以從舞台符號、聲音與表演調度切入,討論海妲・蓋柏樂在父權社會結構下的困境,並結合榮格心理學與馮.法蘭茲對「阿尼姆斯」與「永恆少年」原型的分析,理解女人何以走向精神性的操控、毀滅與死亡。
Thumbnail
本文分析導演巴里・柯斯基(Barrie Kosky)如何運用極簡的舞臺配置,將布萊希特(Bertolt Brecht)的「疏離效果」轉化為視覺奇觀與黑色幽默,探討《三便士歌劇》在當代劇場中的新詮釋,並藉由舞臺、燈光、服裝、音樂等多方面,分析該作如何在保留批判核心的同時,觸及觀眾的觀看位置與人性幽微。
Thumbnail
本文分析導演巴里・柯斯基(Barrie Kosky)如何運用極簡的舞臺配置,將布萊希特(Bertolt Brecht)的「疏離效果」轉化為視覺奇觀與黑色幽默,探討《三便士歌劇》在當代劇場中的新詮釋,並藉由舞臺、燈光、服裝、音樂等多方面,分析該作如何在保留批判核心的同時,觸及觀眾的觀看位置與人性幽微。
Thumbnail
網站開發專案成功的關鍵在於與客戶的有效溝通。本文分享一個成功案例,說明如何透過明確掌握專案需求、主動提供技術方案、定期回報進度、完善技術協助及建立良好客戶關係,順利完成一個中文影片學習分享網站的建置,並獲得客戶高度滿意與後續合作機會。
Thumbnail
網站開發專案成功的關鍵在於與客戶的有效溝通。本文分享一個成功案例,說明如何透過明確掌握專案需求、主動提供技術方案、定期回報進度、完善技術協助及建立良好客戶關係,順利完成一個中文影片學習分享網站的建置,並獲得客戶高度滿意與後續合作機會。
Thumbnail
這是一場從「網路連結 → 線下見面」的活動,我一開始其實有些猶豫,畢竟地點對我來說不近,加上平常在社群裡其實不太主動互動。 但因為主辦人西打誠意滿滿地邀請,甚至還提出補貼車資,最後我決定自費參加。現在回頭看,真的很值得! 場地很有感,氛圍超溫暖 一踏進場地就被暖黃的燈光包圍,小閣樓超舒適,還
Thumbnail
這是一場從「網路連結 → 線下見面」的活動,我一開始其實有些猶豫,畢竟地點對我來說不近,加上平常在社群裡其實不太主動互動。 但因為主辦人西打誠意滿滿地邀請,甚至還提出補貼車資,最後我決定自費參加。現在回頭看,真的很值得! 場地很有感,氛圍超溫暖 一踏進場地就被暖黃的燈光包圍,小閣樓超舒適,還
Thumbnail
背景:從冷門配角到市場主線,算力與電力被重新定價   小P從2008進入股市,每一個時期的投資亮點都不同,記得2009蘋果手機剛上市,當時蘋果只要在媒體上提到哪一間供應鏈,隔天股價就有驚人的表現,當時光學鏡頭非常熱門,因為手機第一次搭上鏡頭可以拍照,也造就傳統相機廠的殞落,如今手機已經全面普及,題
Thumbnail
背景:從冷門配角到市場主線,算力與電力被重新定價   小P從2008進入股市,每一個時期的投資亮點都不同,記得2009蘋果手機剛上市,當時蘋果只要在媒體上提到哪一間供應鏈,隔天股價就有驚人的表現,當時光學鏡頭非常熱門,因為手機第一次搭上鏡頭可以拍照,也造就傳統相機廠的殞落,如今手機已經全面普及,題
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News