iT邦幫忙

2025 iThome 鐵人賽

DAY 4
0
Modern Web

30 天製作工作室 SaaS 產品 (前端篇)系列 第 4

Day4:Zustand 狀態管理與型別安全的 API 層

  • 分享至 

  • xImage
  •  

為什麼選擇 Zustand?

在前端狀態管理的選擇上,我經歷過從 Redux 到 Context API 的演變。對於 Kyo-Dashboard 這樣的管理介面,我選擇 Zustand 的原因:

與其他方案比較

狀態管理 檔案大小 學習曲線 TypeScript 支援 適用場景
Redux Toolkit 陡峭 優秀 大型應用
Zustand 平緩 優秀 中小型應用 ✅
Jotai 中等 優秀 原子化狀態
Context API 簡單 一般 簡單狀態

選擇 Zustand 的理由

  • 極簡 API:不需要 provider 包裝
  • TypeScript 友善:完整的型別推斷
  • 效能優異:只有相關組件會重新渲染
  • 開發體驗佳:Redux DevTools 支援

現有專案架構

目前 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 開始建立狀態管理

基於現有的 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
    });
  }
}));

使用 Store 的簡單組件範例

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 組件:發送和驗證表單
錯誤處理:基本的錯誤狀態管理

目前的架構特點

優點

  • 從現有 API逐步建立狀態管理
  • 邏輯簡單,容易理解和維護
  • TypeScript 支援良好

後續改進方向

  • 加入更複雜的狀態管理(模板、歷史記錄)
  • 實作選擇器優化
  • 引入 UI 組件庫(Mantine)

下一步規劃

明天(Day5)我們將:

  1. 引入 Mantine UI 組件庫
  2. 優化表單的使用者體驗
  3. 加入基本的模板管理功能

上一篇
Day3:初始化前端專案與開發環境設定
系列文
30 天製作工作室 SaaS 產品 (前端篇)4
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言