實戰範例 03:Class 轉 Hooks 語法重構
將老舊的 React Class Component 重構為現代 Functional Component,使用 Hooks 重新實作生命週期,確保功能無損並優化效能。
/opsx:explore/opsx:propose/opsx:apply/opsx:verify/opsx:archive 實戰背景
隨著 React 的演進,函數式元件 (Functional Component) 配合 React Hooks 已經成為主流的開發正規。相比舊有的類別元件 (Class Component),Hooks 能以更簡潔的代碼實現更強大的狀態邏輯複用。
本範例中,我們將重構專案中的使用者資訊顯示元件 UserProfile.tsx。該元件目前包含了繁複的 componentDidMount、componentDidUpdate 以及 setState 邏輯。我們將在不改變其 Props 介面、不影響外部使用的前提下,將其重構為 Hooks 結構。我們使用 完整嚴格工作流 (Full Flow) 來進行此項重構。
規格定義
本次變更計畫的 Capability 為 user-interface。我們預計在規格增量 (Spec Delta) 中定義以下驗收標準:
## MODIFIED Requirements
### Requirement: UserProfile modern component
The site SHALL render user profile information using functional components and react hooks, maintaining backward compatibility.
#### Scenario: Update profile on user ID change
- **WHEN** the `userId` prop changes
- **THEN** the component SHALL fetch new user details and update the render without memory leaks
端到端 6 輪 AI 協作紀錄
以下是 You 與 AI 助手的完整對話歷史。
── 第 1 輪:探索與分析既有 Class 代碼 ──
- You:
/opsx:explore "我們想重構 src/components/UserProfile.tsx,現在它是個 Class Component。幫我看一下它的生命週期跟狀態是怎麼寫的。" - AI:
好的,已進入探索模式。我已讀取
UserProfile.tsx的原始碼:- 該元件儲存了
loading、user與error三個狀態。 - 生命週期處理:
- 在
componentDidMount中,呼叫this.fetchUserData(this.props.userId)。 - 在
componentDidUpdate(prevProps)中,檢查if (prevProps.userId !== this.props.userId),若不同則再次呼叫this.fetchUserData(this.props.userId)。
- 在
重構設計方案:
- 我們可以將其重構為
const UserProfile = ({ userId }: Props) => { ... }。 - 使用
useState宣告狀態。 - 使用一個
useEffect監聽[userId]的變化,來合併componentDidMount與componentDidUpdate的資料加載邏輯。
如果您同意此重構方向,請退出探索模式並建立 proposal。
- 該元件儲存了
── 第 2 輪:建立提案 ──
- You:
/opsx:propose "將 UserProfile 重構為 React Hooks 函數式元件,保持 Props 介面相容" - AI:
好的,已建立變更計畫
refactor-user-profile-to-hooks:- 建立 proposal.md: 說明為什麼要進行這次重構。
- 建立 specs/user-interface/spec.md: 新增相容性與更新邏輯的驗收標準 (AC)。
- 建立 tasks.md: 包含以下任務:
- 1.1 轉換元件聲明與 State 宣告
- 1.2 使用
useEffect重新實作資料加載與生命週期 - 1.3 使用
useCallback優化 fetch 方法,避免多餘渲染 - 1.4 執行測試確保行為一致
請執行
/opsx:apply開始實作。
── 第 3 輪:實作任務 1 (State 轉換) ──
- You:
/opsx:apply - AI:
Implementing: refactor-user-profile-to-hooks (schema: spec-driven)
正在處理任務 1/4:轉換元件聲明與 State 宣告。
我已在
src/components/UserProfile.tsx中將 Class 語法改為 Functional 語法,並宣告狀態:import React, { useState } from 'react'; interface Props { userId: string; } export const UserProfile = ({ userId }: Props) => { const [user, setUser] = useState<User | null>(null); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); // ... 剩餘邏輯 };✓ 任務已完成。已將 tasks.md 中的任務 1.1 標記為完成。
接下來將進行
useEffect的生命週期重構。是否繼續?
── 第 4 輪:實作任務 2 (useEffect 生命週期) ──
- You:
繼續實作生命週期重構。 - AI:
Implementing: refactor-user-profile-to-hooks
正在處理任務 2/4:使用
useEffect重新實作資料加載與生命週期。我已實作了
useEffect邏輯。這能同時解決componentDidMount與componentDidUpdate兩個時期的 API 觸發,且在userId變更時,若前一次請求尚未完成,我們會利用 AbortController 進行取消,以避免 Race Condition (競態條件) 與 Memory Leak (記憶體洩漏)。useEffect(() => { const controller = new AbortController(); async function loadData() { setLoading(true); try { const res = await fetch(`/api/users/${userId}`, { signal: controller.signal }); const data = await res.json(); setUser(data); } catch (err: any) { if (err.name !== 'AbortError') setError(err.message); } finally { setLoading(false); } } loadData(); return () => controller.abort(); // 清除函式,取消前次請求 }, [userId]);✓ 任務已完成。已將任務 1.2 標記為完成。
下一步將使用
useCallback優化方法渲染效能。是否繼續?
── 第 5 輪:實作任務 3 (useCallback 優化) ──
- You:
繼續優化 fetch 方法。 - AI:
Implementing: refactor-user-profile-to-hooks
正在處理任務 3/4:使用
useCallback優化方法,避免多餘渲染。我將元件內部的其他輔助方法(例如手動點擊重新載入按鈕所呼叫的
handleRefresh)使用useCallback進行包裹,防止子元件因父元件重新渲染而被迫重複渲染。const handleRefresh = useCallback(() => { triggerReload(userId); }, [userId]);✓ 任務已完成。已將任務 1.3 標記為完成。
剩下最後的驗證測試。是否繼續?
── 第 6 輪:撰寫測試、驗證與歸檔 ──
- You:
執行測試,並驗證歸檔。 - AI:
Implementing: refactor-user-profile-to-hooks
正在處理任務 4/4:執行測試確保行為一致。
我在
tests/UserProfile.test.tsx中模擬了 PropsuserId變更的測試,確認元件能正確加載新資料,且舊的請求被中斷。測試已全數通過。✓ 4/4 任務全數完成。
自動驗證與歸檔:
- 驗證成功:重構後功能與原有 Class 元件百分之百相容,無 Regression (退化)。
- 智慧同步:已將暫存規格合併至主規格 openspec/specs/user-interface/spec.md。
- 目錄歸檔:已移動計畫資料夾至
openspec/changes/archive/2026-07-14-refactor-user-profile-to-hooks/。
Archive Complete
變更計畫已成功關閉與歸檔!工作目錄目前非常乾淨。
程式碼與設計亮點
- Race Condition 阻絕:採用 HTML5 的
AbortController解決了網絡超時所帶來的競態問題,防止慢速 API 的結果意外覆蓋了最新使用者資訊。 - 單一依賴陣列:只把
[userId]放入副作用副作用 (Side Effect) 依賴,確保只有在 ID 改變時才請求資料,大幅降低了不必要的 API 呼叫。 - Props 相容:外部元件在調用
<UserProfile userId={...} />時,無須做任何代碼修改,完美實現了漸進式重構。