在昨天的實作中,我們成功地將「個人檔案頁」與「課表頁」的職責分離,並順利透過 Cloud Run 拿到了 AI 為東京馬拉松量身打造的結構化 JSON 課表。
但隨即發現了一個問題:只要使用者滑掉 APP、或是 Expo Go 重新載入,產生的 AI 課表與個人設定就會瞬間消失!
在真實的應用場景中,跑者不可能每次打開 APP 都要重新輸入資料、點擊「生成課表」並傻傻等待 AI 運算 5~10 秒。我們必須讓 APP 擁有**「全域狀態 (Global State)」讓不同頁面共享資料,並且具備「持久化記憶 (Persist Storage)」**,把資料實實在在地寫入手機的本地儲存空間中。
今天,我們將透過 Zustand 與它的 Persist 中介軟體,一次解決這兩個痛點!
在 React Native 生態系中,要處理跨頁面的資料共享,常見的幾種選擇:
AsyncStorage.setItem 與 getItem。<Provider>,最厲害的是它內建了強大的 Middleware (中介軟體) 系統。我們只需要掛上 persist,它就能全自動地在背景幫我們把狀態同步到手機硬碟中!因此我最後選擇使用 Zustand!
打開終端機,透過 Expo CLI 安裝 Zustand 以及 React Native 官方推薦的非同步儲存套件:
npx expo install @react-native-async-storage/async-storage zustand
接下來,我們要建立這個具備持久化能力的大腦,需求Prompt如下:
「我正在使用 Expo 開發 React Native APP,並使用 Zustand 進行狀態管理。 請幫我建立一個名為
useStore.ts的全域狀態,需求:
- 包含三個核心狀態:
runnerProfile(存跑者暱稱、身高體重、目標等)、aiPlan(存 AI 課表 JSON)、selectedWeek(記錄目前課表頁選中第幾週,預設 1)。- 提供對應的更新 Actions 以及一個重置狀態的
clearStore函式。- 重點:結合 Zustand 的
persist中介軟體與@react-native-async-storage/async-storage,設定儲存名稱為kakeru-storage,並加入onRehydrateStorage回呼函式,讓快取從手機硬碟還原時能印出日誌確認。」
迅速為產出了具備 TypeScript 型別與持久化中介軟體的 src/store/useStore.ts:
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
export interface RunnerProfile {
nickname: string;
height: number | string;
weight: number | string;
targetRace?: string;
weeklyDays?: number | string;
}
export interface StoreState {
runnerProfile: RunnerProfile | null;
aiPlan: any | null;
selectedWeek: number;
setRunnerProfile: (profile: Partial<RunnerProfile> | null) => void;
setAiPlan: (plan: any | null) => void;
setSelectedWeek: (week: number) => void;
clearStore: () => void;
}
const defaultRunner: RunnerProfile = {
nickname: 'Takuya',
height: 171,
weight: 68,
targetRace: '東京馬拉松 (目標 Sub 4)',
weeklyDays: 4,
};
const useStore = create<StoreState>()(
persist(
(set) => ({
// --- 狀態定義 ---
runnerProfile: defaultRunner,
aiPlan: null,
selectedWeek: 1,
// --- Actions ---
setRunnerProfile: (data) =>
set((state) => ({
runnerProfile: data ? { ...(state.runnerProfile || defaultRunner), ...data } : null,
})),
setAiPlan: (plan) => set({ aiPlan: plan }),
setSelectedWeek: (week) => set({ selectedWeek: week }),
clearStore: () =>
set({
runnerProfile: null,
aiPlan: null,
selectedWeek: 1,
}),
}),
{
name: 'kakeru-storage', // 儲存在手機 AsyncStorage 的 Key
storage: createJSONStorage(() => AsyncStorage), // 指定 React Native 的儲存引擎
onRehydrateStorage: () => (state) => {
console.log('✅ [Zustand Persist] 快取資料已從手機硬碟成功恢復!', {
hasRunner: Boolean(state?.runnerProfile),
hasPlan: Boolean(state?.aiPlan),
});
},
}
)
);
export default useStore;
在實體手機 Expo Go 上測試時,曾短暫遇到「滑掉重開後仍然要求重新生成」的情況,而這一點只需在終端機重啟 Metro 並清除快取:
npx expo start -c
重啟後重新生成一次課表,資料便會確實寫入手機硬碟!
有了具備持久化能力的 useStore,整個 App 的跨頁面互動變得無比流暢:
當跑者更新身高體重時,直接呼叫 setRunnerProfile,資料在記憶體更新的同時也秒存手機硬碟:
import useStore from '../store/useStore';
export default function ProfileScreen() {
const setRunnerProfile = useStore((state) => state.setRunnerProfile);
const handleSave = () => {
// 立即寫入全域狀態與本地儲存,無需發送耗時的 AI 請求
setRunnerProfile({ nickname, height: Number(height), weight: Number(weight) });
Alert.alert('儲存成功', '跑者基本資料已更新!');
};
}
在課表頁透過 Cloud Run 拿到 AI 回傳的資料後,呼叫 setAiPlan;而週次切換(第 1 ~ 4 週)也直接綁定全域 selectedWeek:
import useStore from '../store/useStore';
export default function ScheduleScreen() {
const { aiPlan, setAiPlan, runnerProfile, selectedWeek, setSelectedWeek } = useStore();
const handleGeneratePlan = async () => {
const apiResponse = await generateMarathonPlan(payload);
// 一行代碼,同時更新全域狀態並非同步寫入手機硬碟!
setAiPlan(apiResponse.data);
};
}
首頁直接透過 Hook 提取 runnerProfile 與 aiPlan,精準響應最新狀態:
import useStore from '../store/useStore';
export default function HomeScreen() {
const runnerProfile = useStore((state) => state.runnerProfile);
const aiPlan = useStore((state) => state.aiPlan);
return (
<Text>歡迎回到 KAKERU, {runnerProfile?.nickname}</Text>
);
}
把程式碼存檔後,在實體手機的 Expo Go 進行驗證:
終端機印出:
[Zustand Persist] 快取資料已從手機硬碟成功恢復! { hasRunner: true, hasPlan: true }
今天我們透過 Zustand + Persist 完成了 App 的升級:
@react-native-async-storage/async-storage 實現了離線快取與本地持久化儲存。有了這個能夠永久記憶的全域大腦,明天(Day 19)將把重點轉移到視覺與互動體驗上,做一個看起來比較有質感的 AI 教練評估卡片、可平滑切換的週次選擇器,以及精緻的每日訓練任務清單!我們明天見!