實戰背景
隨著 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={...} />時,無須做任何代碼修改,完美實現了漸進式重構。