在前端狀態管理的選擇上,我經歷過從 Redux 到 Context API 的演變。對於 Kyo-Dashboard 這樣的管理介面,我選擇 Zustand 的原因:
與其他方案比較:
狀態管理 | 檔案大小 | 學習曲線 | TypeScript 支援 | 適用場景 |
---|---|---|---|---|
Redux Toolkit | 大 | 陡峭 | 優秀 | 大型應用 |
Zustand | 小 | 平緩 | 優秀 | 中小型應用 ✅ |
Jotai | 小 | 中等 | 優秀 | 原子化狀態 |
Context API | 無 | 簡單 | 一般 | 簡單狀態 |
選擇 Zustand 的理由:
目前 kyo-dashboard
的狀況:
// apps/kyo-dashboard/src/lib/api.ts (已存在)
export async function sendOtp(baseUrl: string, body: { phone: string; templateId?: number }) {
const res = await fetch(`${baseUrl}/api/otp/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
return handle<{ msgId: string; status: string }>(res);
}
export async function verifyOtp(baseUrl: string, body: { phone: string; otp: string }) {
const res = await fetch(`${baseUrl}/api/otp/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
return handle<{ success: boolean; attemptsLeft?: number }>(res);
}
目前已經有基本的 API 函數,但還沒有 Zustand store。
基於現有的 API 函數,我們可以逐步引入狀態管理。先從簡單的開始:
1. 建立基礎型別定義
// apps/kyo-dashboard/src/types/api.ts (基於現有 API)
export interface SendOtpRequest {
phone: string;
templateId?: number;
}
export interface VerifyOtpRequest {
phone: string;
otp: string;
}
export interface SendOtpResponse {
msgId: string;
status: string;
}
export interface VerifyOtpResponse {
success: boolean;
attemptsLeft?: number;
}
2. 建立簡單的 OTP Store
// apps/kyo-dashboard/src/stores/otpStore.ts
import { create } from 'zustand';
import { sendOtp, verifyOtp } from '../lib/api';
import type { SendOtpRequest, VerifyOtpRequest } from '../types/api';
interface OtpState {
// OTP 發送狀態
sendLoading: boolean;
sendError: string | null;
lastSentResult: { msgId: string; phone: string } | null;
// OTP 驗證狀態
verifyLoading: boolean;
verifyError: string | null;
}
interface OtpActions {
// OTP 操作
sendOtpAction: (request: SendOtpRequest) => Promise<{ success: boolean; msgId?: string }>;
verifyOtpAction: (request: VerifyOtpRequest) => Promise<{ success: boolean }>;
// 清理操作
clearErrors: () => void;
resetState: () => void;
}
type OtpStore = OtpState & OtpActions;
export const useOtpStore = create<OtpStore>((set, get) => ({
// 初始狀態
sendLoading: false,
sendError: null,
lastSentResult: null,
verifyLoading: false,
verifyError: null,
// OTP 發送
sendOtpAction: async (request) => {
set({ sendLoading: true, sendError: null });
try {
const baseUrl = 'http://localhost:3000'; // 開發環境
const result = await sendOtp(baseUrl, request);
set({
sendLoading: false,
lastSentResult: { msgId: result.msgId, phone: request.phone }
});
return { success: true, msgId: result.msgId };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '發送失敗';
set({
sendLoading: false,
sendError: errorMessage
});
return { success: false };
}
},
// OTP 驗證
verifyOtpAction: async (request) => {
set({ verifyLoading: true, verifyError: null });
try {
const baseUrl = 'http://localhost:3000';
const result = await verifyOtp(baseUrl, request);
set({ verifyLoading: false });
return { success: result.success };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '驗證失敗';
set({
verifyLoading: false,
verifyError: errorMessage
});
return { success: false };
}
},
// 清理錯誤
clearErrors: () => {
set({
sendError: null,
verifyError: null
});
},
// 重置狀態
resetState: () => {
set({
sendLoading: false,
sendError: null,
lastSentResult: null,
verifyLoading: false,
verifyError: null
});
}
}));
3. 建立 OTP 發送表單
// apps/kyo-dashboard/src/components/OtpSendForm.tsx
import { useState } from 'react';
import { useOtpStore } from '../stores/otpStore';
export function OtpSendForm() {
const [phone, setPhone] = useState('');
const { sendOtpAction, sendLoading, sendError } = useOtpStore();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!phone) return;
const result = await sendOtpAction({ phone });
if (result.success) {
alert(`驗證碼已發送!Message ID: ${result.msgId}`);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>手機號碼:</label>
<input
type="text"
value={phone}
onChange={e => setPhone(e.target.value)}
placeholder="09XXXXXXXX"
disabled={sendLoading}
/>
</div>
{sendError && <div style={{ color: 'red' }}>{sendError}</div>}
<button type="submit" disabled={sendLoading}>
{sendLoading ? '發送中...' : '發送驗證碼'}
</button>
</form>
);
}
4. 驗證表單組件
// apps/kyo-dashboard/src/components/OtpVerifyForm.tsx
import { useState } from 'react';
import { useOtpStore } from '../stores/otpStore';
export function OtpVerifyForm({ phone }: { phone: string }) {
const [otp, setOtp] = useState('');
const { verifyOtpAction, verifyLoading, verifyError } = useOtpStore();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!otp) return;
const result = await verifyOtpAction({ phone, otp });
if (result.success) {
alert('驗證成功!');
setOtp('');
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>驗證碼(發送至 {phone}):</label>
<input
type="text"
value={otp}
onChange={e => setOtp(e.target.value)}
placeholder="請輸入 6 位數驗證碼"
maxLength={6}
disabled={verifyLoading}
/>
</div>
{verifyError && <div style={{ color: 'red' }}>{verifyError}</div>}
<button type="submit" disabled={verifyLoading || !otp}>
{verifyLoading ? '驗證中...' : '驗證'}
</button>
</form>
);
}
✅ 基礎 Zustand Store:簡單的狀態管理架構
✅ API 整合:使用現有的 API 函數
✅ 型別安全:基本的 TypeScript 支援
✅ React 組件:發送和驗證表單
✅ 錯誤處理:基本的錯誤狀態管理
優點:
後續改進方向:
明天(Day5)我們將: