史上最漫長的React hooks遷移之旅:如何在不停產的情況下大規模現代化遺留代碼庫
我們如何在不中斷產品開發的情況下(最終)實現了大規模遺留代碼庫的現代化改造
在Faire,我們最近完成了前端代碼庫歷史上最漫長的一次代碼遷移。我們將最大的React應用從類組件和MobX類遷移到了函數組件和hooks。這次遷移通過提升代碼組合性和訪問強大的第三方庫(如TanStack Query和zustand),顯著增強了代碼庫的可維護性。同時借助數據預取提升了網站性能,并通過NextJS為采用服務器組件做好了準備。
以下是我們的遷移歷程,以及我們如何在不影響產品開發的情況下完成這一壯舉。
等待的代價
在深入遷移細節之前,讓我們直面核心問題:"為什么hooks在2019年發布,我們卻等到2025年才完成遷移?"2017年時,我們的React應用使用Redux,面臨著常見挑戰——包裝器地獄、代碼復用性差、副作用管理復雜以及笨重的reducer。
2018年,我們轉向MobX,因為它與類組件配合使用的編程模型要簡單得多。當hooks在2019年2月發布時,我們剛完成MobX遷移,對hooks的持久性存疑。考慮到當時的快速增長和產品需求,再次遷移似乎并不明智。
然而我們等待得太久了。到2022年我們最終決定從類遷移到函數時,代碼庫中的類數量已增長至5倍,而前端團隊僅擴大了一倍。這種增長極大地增加了遷移的復雜性和范圍。
為何必須改變
雖然類組件和MobX的組合最初表現良好,但隨著網站功能日益豐富,兩個主要問題逐漸顯現:
- 可維護性:由于缺乏強制性的類結構標準,代碼庫中積累了大量"大雜燴"類和過度聰明的面向對象編程模式。
- 性能:由于包體積增大和缺乏適當客戶端緩存的數據獲取層,網站性能下降。此外,MobX使得服務器端渲染的實現頗具挑戰性。
可維護性問題
我們的代碼庫包含兩種主要類型的MobX類:
- "視圖狀態/模型":實例化一次后作為props傳遞
// State.ts
classReferralViewState {
@observable// 客戶端狀態
activeTab: Tab = Tab.INVITED;
@observable// 服務器狀態
referralResponses: Record<Tab, IPromiseBasedObservable<Referral[]>> = {};
@computed// 派生狀態
getreferrals(): Referral[] | undefined {
returnthis.referralResponses[this.activeTab].case({
fulfilled: (referrals) => referrals,
pending: () =>undefined,
})
}
// 數據獲取邏輯
asyncfetchReferrals() {
try {
if(!this.referralResponses[this.activeTab]) {
this.referralResponses[this.activeTab] = fromPromise(
fetchReferrals(
IListReferredBrandsRequest.build({
active_tab: this.activeTab,
})
)
)
}
} catch (error) {
console.error(error);
}
}
}- "單例存儲":實例化一次(附加到window對象)并在應用任何位置訪問
// Store.ts
classUserStore {
// 將實例綁定到全局window
static get = singletonGetter(UserStore);
@observable// 從服務器序列化的初始值
user: IUser = getUser();
@computed// 派生值
getfullName() {
return`${user.first_name} ${user.last_name}`;
}
// 數據獲取
refetchUser = async () => {
try {
this.user = awaitfetchUser();
} catch (error) {
console.error(error);
}
}
}隨著代碼庫增長,這種方法變得難以維護。視圖狀態實例通過整個子樹傳遞,導致開發者傾向于直接向這些類添加新狀態或派生值,造成簽名臃腫和組件接口僵化。
單例存儲也存在類似問題——全局可訪問性使得添加新字段比創建專用存儲更方便。雖然這種與組件樹的分離提供了靈活性,但也更容易引入錯誤。由于這些存儲早于React的"hooks規則",有時會因頂層訪問和變更導致難以追蹤的錯誤。
性能問題
這種架構方式導致包體積膨脹,因為訪問單例存儲會將其全部代碼拉入頁面包中。
我們還需要為每個客戶端-服務器端點調用自行實現數據獲取、緩存和失效邏輯。像TanStack Query和SWR這樣的現代基于hooks的庫與我們的類組件不兼容。
此外,我們的"單例存儲"window技巧使服務器端渲染復雜化,因為window在服務器上不可用,且我們需要防止狀態在請求間泄漏。在轉向NextJS之前,我們嘗試自行實現React SSR,不得不使用zone.js作為臨時解決方案。
遷移策略
2022年7月,我們決定遷移到hooks。由于當時AI代碼生成技術尚不成熟(GitHub Copilot剛剛起步,ChatGPT尚未出現),我們計劃采用傳統遷移方法。
為了追蹤使用情況,我們創建了報告類組件和MobX類的ESLint規則,將它們添加到單獨的eslint.tracking.config.js中。這種配置讓我們可以在CI中監控違規情況并向Snowflake報告,而不會用警告干擾開發者的IDE。由于我們的源文件映射到產品區域和團隊,我們構建了展示各團隊遷移進度的儀表板。
我們還需要從不支持hooks的mobx-react v5升級。雖然升級mobx-react相對簡單,但由于我們的多倉庫包結構和TypeScript的useDefineForClassFields影響類行為,遷移到mobxv6頗具挑戰性。最終我們停留在mobx v5和mobx-react v6的組合。
圖片
兼容性矩陣顯示mobx-react v5不支持hooks,但mobx-react v6支持
我們添加了便利工具來簡化遷移同時保持互操作性。一個關鍵補充是createStoreHook工廠,它將單例存儲轉換為帶有選擇器參數的hook,使我們能夠先遷移存儲使用者,再更新底層實現。
在開始大規模遷移前,我們創建了針對Faire的MobX類轉hooks指南。由于到2022年hooks已經成熟,我們可以參考大量資料,包括新的react.dev文檔。
推進遷移
工具和文檔就位后,我們轉向實際的代碼遷移。雖然當時的AI工具還不夠成熟,無法可靠處理轉換,但我們找到了部分自動化的方法。
我們與Grit合作,他們使用專門的查詢語言進行代碼模式匹配和轉換。他們的團隊已經開發了將類組件轉換為函數組件的模式,并調整以處理MobX裝飾器。我們還創建了自己的模式來將"視圖狀態"MobX類轉換為自定義hooks。這些自動化模式要么一次性完成遷移,要么為手動優化提供良好起點。
圖片
動畫展示Grit將React類組件遷移為函數組件的過程
然而,遷移的瓶頸并非代碼轉換本身——而是測試和審查變更。盡管我們在互操作性方面取得了良好進展,但我們的應用并非為hooks設計,且將MobX的可變observable推向了極限。
盡管前端代碼庫有很高的測試覆蓋率(平均80%以上),我們還是遇到了許多由新產生的不穩定引用導致的useEffect無限循環。這促使我們要求對所有非簡單的hook遷移PR進行手動測試,并由"hooks專家"審查。雖然這延長了遷移過程,但考慮到這些遷移的復雜性,這被證明是正確的決定。
獲得支持
這次遷移最困難的部分之一是獲得工程師和領導層的支持。
當我們首次宣布hooks遷移時,反應褒貶不一。一些工程師感到興奮,而另一些則心存疑慮。乍看之下,hooks似乎是一種奇怪的范式轉變,有著特殊的規則,相比類組件的優勢也不明顯。直到我們在API層引入TanStack的useQuery后,這種情況才改變。
圖片
表情包展示一只鸚鵡最初對React hooks不滿,直到useQuery出現
useQuery極大地簡化了React應用中最常見的挑戰:數據獲取。相比基于類的方法,react-query API使服務器狀態管理簡單得多。改進如此顯著,以至于開發者開始自愿將類組件遷移為函數,只為使用useQuery。
盡管有部分自動化,遷移成本仍然太高,無法強制要求。由于我們的團隊圍繞實現公司目標的路線圖構建,技術債務分布并不均勻。一些團隊在幾個月內完成了遷移,而擁有遺留或復雜代碼的團隊則需要更多時間。
我們嘗試了各種激勵措施鼓勵團隊完成遷移,但效果有限。我們的遷移排行榜最初激發了一些熱情,但少數熱心貢獻者很快占據了前列,使其他人難以競爭。我們還添加了修改類時的CI警告,但由于大多數遷移需要專門的PR,這些警告常被忽視。
到2025年上半年,我們終于獲得批準將遷移設為強制要求。那時AI工具已顯著改進,剩余的類也不多,即使沒有強制要求,我們可能也會完成遷移。
多年來的React類組件和MobX類數量下降圖表
遷移影響
單個類遷移到函數并沒有立即釋放太多價值。但當較大代碼區域和應用集體遷移到hooks后,效益開始累積。
當我們的數據獲取層使用useQuery后,我們開始利用推測性數據預取來改善頁面導航的加載時間。這被用于所有關鍵用戶旅程,僅預取帶來的速度提升就累計為我們市場帶來了5%的訂單量增長。
由于MobX自行處理狀態管理,它與React編譯器不兼容。移除MobX后,我們開始實驗為市場啟用編譯器,觀察到全站INP提升了25%。
MobX v5也與Turbopack不兼容(即使在支持舊裝飾器后),移除它解鎖了更快的本地開發打包器,為工程師每天節省約20-30分鐘。
展望未來
我們仍在從其他大型React應用中移除MobX,但現在有了AI的幫助。過去一年,LLM的編碼能力顯著提升,現在能生成更高質量的從類組件和MobX到函數組件和hooks的遷移。我們已成功使用Cursor的Agent模式加速遷移,目前正在試驗各種"后臺代理"解決方案來異步執行遷移。
結語
我們從類組件和MobX到React hooks的遷移不僅僅是一次重構——它是我們大規模構建UI方式的深度現代化。多年來,我們沿用過去行之有效的模式,但這些模式阻礙了我們獲得現代React提供的性能和組合性優勢。遷移一個成熟的、重度依賴類的代碼庫并不光鮮,但一旦完成,這種工程工作每天都會帶來回報。
我們沒有輕視這段旅程。它需要周密的計劃、定制的linting基礎設施、量身定制的代碼轉換和大量手動測試。還需要組織的耐心和共同信念——這種基礎性工作很重要。作為回報,我們現在擁有了一個更易理解、模塊化程度更高、兼容React生態最佳工具的代碼庫。
如果你在自己的代碼庫面臨類似遷移:開始測量、從小處著手,并在工具和人員上投入。因為最終,你會想構建一些hooks-based React才能實現的東西——當那一刻到來時,你會慶幸基礎已經準備就緒。
原文地址:https://craft.faire.com/the-worlds-longest-react-hooks-migration-8f357cdcdbe9
作者: Chris Krogh




























