iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
佛心分享-SideProject30

我的時間到底去哪裡了!? – 個人時間數據系統開發挑戰系列 第 14

前端 Activities CRUD — React + TypeScript 程式實作

  • 分享至 

  • xImage
  •  

前言

昨天我們探討了前端 Activities 管理系統的 UI/UX 設計,今天來看看如何用程式碼實現這些設計。這篇文章會專注於「為什麼這樣寫」和「程式碼背後的思考」。

技術架構設計

為什麼要分層架構?

就像蓋房子一樣,我們需要不同的「樓層」來組織程式碼:

src/
├── components/    # UI 組件層 - 像樂高積木
├── services/      # 服務層 - 與後端溝通的橋樑
├── contexts/      # 狀態管理層 - 所有組件會用到的數據與邏輯
└── types/         # 類型定義層 - 資料格式的字典

組件層:負責 UI 顯示,像樂高積木一樣可重複使用
服務層:負責與後端 API 溝通,處理數據的取得和轉換
狀態管理層:負責所有組件共用的數據和邏輯
類型定義層:定義資料格式,讓整個專案有一致的資料結構

TypeScript 類型系統設計

為什麼需要類型定義?

想像一下,如果每個人都用不同的格式來描述「活動」,會發生什麼事?有人說活動名稱叫 name,有人說叫 title,這樣就會造成混亂。

TypeScript 的類型定義就像「資料字典」,告訴整個專案「活動」應該長什麼樣子:

// 定義活動的資料結構
export interface Activity {
  id: string; // 唯一識別碼
  name: string; // 活動名稱
  totalTime: number; // 總時間(分鐘)
  weeklyTime: number; // 本週時間(分鐘)
  targetTime: number; // 目標時間(分鐘)
  color: string; // 活動顏色
  icon: ActivityIconType; // 活動圖示
}

好處

  • 所有組件都用相同的資料格式
  • 寫錯屬性名稱時,TypeScript 會提醒你
  • IDE 會自動提示可用的屬性

組件 Props 設計思路

每個組件都需要「接收什麼資料」和「提供什麼功能」的說明:

// 活動卡片需要什麼?
interface ActivityCardProps {
  activity: Activity; // 要顯示的活動資料
  onClick?: () => void; // 點擊卡片時要做什麼
  onEdit?: (activity: Activity) => void; // 點擊編輯時要做什麼
  onDelete?: (activity: Activity) => void; // 點擊刪除時要做什麼
}

這樣設計的好處是:組件只專注於顯示,具體要做什麼由父組件決定。

狀態管理策略

為什麼需要狀態管理?

想像一下,如果每個組件都要自己記住「用戶是否已登入」,會發生什麼事?每個組件都要重複寫相同的邏輯,而且資料可能不同步。

我們用兩種方式管理狀態:

1. 全域狀態(Context):所有組件都需要知道的資料

  • 用戶登入狀態
  • 錯誤訊息
  • 主題設定

2. 本地狀態(useState):只有特定組件需要的資料

  • 表單輸入內容
  • Modal 開關狀態
  • 載入狀態

實際例子

// 錯誤處理 - 全域狀態
const ErrorContext = createContext();

// 任何組件都可以顯示錯誤
const SomeComponent = () => {
  const { addError } = useError();

  const handleError = () => {
    addError({ message: "發生錯誤了!" });
  };
};

// 表單狀態 - 本地狀態
const CreateActivityModal = () => {
  const [formData, setFormData] = useState({
    name: "",
    targetTime: "",
  });
};

這樣設計的好處是:資料不會重複,也不會不同步。

API 服務層設計

為什麼要統一 API 服務?

如果每個組件都直接呼叫 API,會發生什麼問題?

  • 每個組件都要處理認證、錯誤處理
  • 重複的程式碼很多
  • 修改 API 邏輯時要改很多地方

我們建立一個「API 服務層」,就像一個「翻譯官」,負責:

  • 加上認證標頭
  • 統一錯誤處理
  • 處理資料轉換

設計思路

// 統一的 API 服務
const useApi = () => {
  // 自動加上認證標頭
  const headers = {
    "Content-Type": "application/json",
    ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {})
  };

  // 統一處理錯誤
  const handleError = (error) => {
    if (error.status === 401) {
      logout(); // 自動登出
    }
    addError({ message: "API 呼叫失敗" });
  };

  return { get, post, put, delete };
};

// 專用的活動服務
const useActivities = () => {
  const api = useApi();

  return {
    getActivities: () => api.get("/api/activities"),
    createActivity: (data) => api.post("/api/activities", data),
    updateActivity: (id, data) => api.put(`/api/activities/${id}`, data),
    deleteActivity: (id) => api.delete(`/api/activities/${id}`)
  };
};

這樣設計的好處是:組件只需要呼叫 createActivity(data),不用管認證、錯誤處理等細節。

核心功能實作

1. 查看活動 - ActivityCard 組件

設計思路:活動卡片就像「名片」,要能清楚顯示活動資訊,也要提供操作按鈕。

關鍵設計

  • 單一職責:只負責顯示一個活動
  • 可重用性:可以在任何地方使用
  • 事件分離:點擊卡片和點擊按鈕是不同的動作
const ActivityCard = ({ activity, onClick, onEdit, onDelete }) => {
  // 防止按鈕點擊觸發卡片點擊
  const handleEdit = (e) => {
    e.stopPropagation(); // 關鍵:阻止事件冒泡
    onEdit(activity);
  };

  return (
    <div onClick={onClick}>
      <h3>{activity.name}</h3>
      <button onClick={handleEdit}>編輯</button>
      <button onClick={handleDelete}>刪除</button>
    </div>
  );
};

為什麼要 stopPropagation()
如果沒有這個,點擊編輯按鈕時會同時觸發卡片的點擊事件,造成意外跳轉。

2. 新增活動 - CreateActivityModal 組件

設計思路:Modal 就像「彈出式表單」,要能收集用戶輸入,也要處理表單驗證。

關鍵設計

  • 表單狀態管理:用一個物件管理所有輸入
  • 資料轉換:將表單資料轉換成 API 需要的格式
  • 重置機制:關閉時清空表單
const CreateActivityModal = ({ isOpen, onSubmit }) => {
  const [formData, setFormData] = useState({
    name: "",
    targetTime: "",
    color: PRESET_COLORS[0],
    icon: PRESET_ICONS[0],
  });

  const handleSubmit = (e) => {
    e.preventDefault();

    // 轉換資料格式
    const newActivity = {
      ...formData,
      targetTime: parseInt(formData.targetTime), // 字串轉數字
      totalTime: 0, // 新活動預設值
      weeklyTime: 0,
    };

    onSubmit(newActivity);
  };
};

為什麼要轉換資料格式?
表單輸入都是字串,但 API 需要數字,所以要手動轉換。

3. 編輯活動 - EditActivityModal 組件

設計思路:編輯 Modal 與新增類似,但需要「預填現有資料」。

關鍵差異

  • 預填資料:開啟時填入現有活動的資料
  • 部分更新:只更新有變更的欄位
const EditActivityModal = ({ activity, onSubmit }) => {
  const [formData, setFormData] = useState({});

  // 當活動資料變更時,更新表單
  useEffect(() => {
    if (activity) {
      setFormData({
        name: activity.name,
        targetTime: activity.targetTime.toString(),
        color: activity.color,
        icon: activity.icon,
      });
    }
  }, [activity]);
};

為什麼要用 useEffect
因為 activity 是從外部傳入的,當它變更時,我們需要同步更新表單。

4. 刪除活動 - DeleteConfirmationModal 組件

設計思路:刪除是危險操作,需要「二次確認」防止誤刪。

關鍵設計

  • 確認機制:顯示要刪除的活動資訊
  • 安全設計:需要明確點擊確認按鈕
const DeleteConfirmationModal = ({ activity, onConfirm }) => {
  return (
    <div>
      <h2>確定要刪除以下活動嗎?</h2>
      <div>
        <span>{activity.icon}</span>
        <span>{activity.name}</span>
        <span>總時間: {activity.totalTime}</span>
      </div>
      <button onClick={onConfirm}>確定刪除</button>
    </div>
  );
};

為什麼要顯示活動資訊?
讓用戶確認刪除的是正確的活動,避免誤刪。

錯誤處理策略

為什麼要統一錯誤處理?

如果每個組件都自己處理錯誤,會發生什麼問題?

  • 錯誤訊息不一致
  • 重複的錯誤處理程式碼
  • 難以維護和修改

我們建立一個「統一錯誤處理機制」:

// 任何地方都可以顯示錯誤
const handleCreateActivity = async (data) => {
  try {
    await createActivity(data);
  } catch (error) {
    addError({
      type: "error",
      title: "創建活動失敗",
      message: "無法創建新活動,請稍後再試",
    });
  }
};

好處

  • 錯誤訊息統一格式
  • 自動顯示 Toast 通知
  • 可以設定自動隱藏時間

程式碼最佳實踐

1. 單一職責原則

每個組件只做一件事:

  • ActivityCard:只負責顯示活動
  • CreateActivityModal:只負責新增表單
  • DeleteConfirmationModal:只負責刪除確認

2. 可重用性設計

// 通用的 Modal 基礎組件
const BaseModal = ({ isOpen, onClose, title, children }) => {
  if (!isOpen) return null;
  return (
    <div className="modal-overlay">
      <div className="modal-content">
        <h2>{title}</h2>
        {children}
      </div>
    </div>
  );
};

好處:所有 Modal 都有相同的樣式和行為,只需要改變內容。

3. 自定義 Hook 提取

// 提取表單邏輯
const useForm = (initialValues) => {
  const [values, setValues] = useState(initialValues);
  const setValue = (key, value) =>
    setValues((prev) => ({ ...prev, [key]: value }));
  const reset = () => setValues(initialValues);
  return { values, setValue, reset };
};

好處:表單邏輯可以在多個組件中重複使用。

總結

今天我們探討了前端 Activities CRUD 功能的程式實作,重點在於:

  1. 分層架構 - 讓程式碼更有組織性
  2. TypeScript 類型系統 - 提供編譯時錯誤檢查
  3. 狀態管理 - 合理分配全域和本地狀態
  4. API 服務層 - 統一處理後端溝通
  5. 組件設計 - 單一職責和可重用性
  6. 錯誤處理 - 統一的用戶體驗

這些設計原則不僅適用於 Activities 功能,也是整個前端應用的基礎架構。


上一篇
Day13 — 前端活動管理的 UI/UX 設計
下一篇
Java 日期時間的三種常見型別比較:Date、LocalDateTime、Instant
系列文
我的時間到底去哪裡了!? – 個人時間數據系統開發挑戰15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言