從 Vue 3 走向 React:資深後端與全端架構師的急速轉譯指南(上篇)
作者筆記: 在 AI-Assisted Coding(Cursor / Claude Code)時代,語法的敲擊已經不再是瓶頸。「語法可以問 AI,但架構與心智模型必須掌握在自己腦中。」本文將帶領熟悉 Vue 3 與 Laravel 的開發者,用最快的速度建立 React 的核心防線。
一、 核心心智模型的碰撞:從響應式(Reactivity)到純函數(Immutability)
身為 Vue 3 (Composition API) 的開發者,我們習慣了透過
ref 和 Proxy 機制在背景自動追蹤狀態、直接修改資料 (user.value.name = 'John'),UI 就會自動更新。但在 React 的世界裡,核心邏輯完全不同:
- $UI = f(state)$(純函數思維): React 認為畫面永遠是當下狀態的函數。
- 絕對不可直接修改 State(Immutability): React 是透過記憶體位址的改變(
Object.is比較)來決定是否觸發 Re-render。如果你寫出list.push(item)或user.name = 'abc',記憶體位址沒變,React 宣告「資料沒換」,畫面就會停在原地不動。- 正確做法: 必須透過 Setter 產生一隻全新的物件或陣列(例如:
setList(prev => [...prev, newItem]))。
二、 生命週期的降維打擊:萬物皆可 useEffect
Vue 3 把生命週期切得非常直覺(
onMounted、onUnmounted、watch)。但在 React 裡,沒有獨立的生命週期函數,一切交給 useEffect。轉換心法:不要去想「組件現在處於哪個生命階段」,而是去想「當某些變數(依賴項)改變或組件出現時,我要執行什麼副作用(Side Effect)」。
- 相當於
onMounted:JavaScriptuseEffect(() => { fetchData(); }, []); // 依賴項為空陣列,代表只跑一次 - 相當於
onMounted+onUnmounted(例如:綁定 Event Listener):JavaScriptuseEffect(() => { window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); // 清理函數 }; }, []); - 相當於
watch:JavaScriptuseEffect(() => { fetchDetail(userId); }, [userId]); // 只要 userId 改變就執行
⚠️ 大魔王陷阱:閉包陷阱 (Stale Closure)如果useEffect內部抓了某個變數卻沒把它放進依賴項陣列,該函式會因為 JavaScript 閉包特性,永遠「鎖死」在第一次建立時的舊值。請務必依賴 ESLint (eslint-plugin-react-hooks) 的紅燈提示來補齊依賴項。
三、 實戰演練:用 React 刻一個高效能互動 DataTable
對標 Filament 或 Vue 表格,我們來看一個具備搜尋、排序與前端分頁的標準 React 組件:
JavaScript
import React, { useState, useMemo } from 'react';
const INITIAL_DATA = [
{ id: 1, name: 'Alex Johnson', email: 'alex@example.com', role: 'Admin', status: 'Active' },
{ id: 2, name: 'Sarah Connor', email: 'sarah@skynet.com', role: 'Editor', status: 'Inactive' },
{ id: 3, name: 'Bruce Wayne', email: 'bruce@wayne.com', role: 'Admin', status: 'Active' },
];
export default function DataTable() {
const [searchQuery, setSearchQuery] = useState('');
const [sortField, setSortField] = useState('id');
const [sortDirection, setSortDirection] = useState('asc');
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 2;
// 使用 useMemo 做快取優化,避免每次 Render 都重新過濾與排序
const filteredAndSortedData = useMemo(() => {
let result = INITIAL_DATA.filter(item =>
item.name.toLowerCase().includes(searchQuery.toLowerCase())
);
result.sort((a, b) => {
if (a[sortField] < b[sortField]) return sortDirection === 'asc' ? -1 : 1;
if (a[sortField] > b[sortField]) return sortDirection === 'asc' ? 1 : -1;
return 0;
});
return result;
}, [searchQuery, sortField, sortDirection]);
const paginatedData = useMemo(() => {
const start = (currentPage - 1) * pageSize;
return filteredAndSortedData.slice(start, start + pageSize);
}, [filteredAndSortedData, currentPage]);
const handleSort = (field) => {
if (sortField === field) {
setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortDirection('asc');
}
};
return (
<div className="p-6 bg-white rounded-xl shadow space-y-4">
<input
type="text"
placeholder="搜尋姓名..."
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
setCurrentPage(1);
}}
className="px-4 py-2 border rounded-lg"
/>
<table className="w-full text-left">
<thead>
<tr>
<th onClick={() => handleSort('name')} className="cursor-pointer">姓名</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{paginatedData.map(row => (
<tr key={row.id} className="border-b">
<td className="p-3">{row.name}</td>
<td className="p-3">{row.email}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
- 架構亮點解析:
- 受控組件 (Controlled Component): 搜尋框的
value與onChange緊密結合 React State。 useMemo取代 Vue Computed: 確保複雜的陣列過濾與排序只在依賴項變動時執行,不浪費運算效能。- 陣列渲染
.map(): 強制帶有唯一的key={row.id},供 React Diffing 演算法高效比對 VDOM。
👉 【上篇總結】
掌握了不可變性、
useEffect 的依賴邏輯與受控組件後,你已經拿下了 React 的核心地圖。從 Vue 3 走向 React:資深後端與全端架構師的急速轉譯指南(下篇)
四、 SPA vs. SSR / RSC:全端架構的選型思維
當你從 Vue/Nuxt 轉換到 React 生態時,最需要切換的是「應用程式的運行邊界」:
- SPA (Vite + React):
- 對標: 純 Vue SPA 或 Laravel API + 獨立前端。
- 運作: 瀏覽器下載靜態 HTML 與大包 JS,所有路由與資料抓取(Client-side Fetching)都在客戶端發生。
- 適用: 後台管理系統 (Admin Dashboard)、內部 SaaS 工具、不需要 SEO 的封閉型 App。
- SSR / RSC (Next.js App Router):
- 對標: Nuxt 3 (SSR 模式) 或 Laravel Blade。
- 運作(React Server Components): 預設所有組件都在 Node.js 伺服器上執行,甚至可以直接連資料庫,跑完後只把乾淨的 HTML/JSON 串流給前端,完全不佔客戶端 JS 體積。只有需要互動的組件才宣告為客戶端組件 (
'use client')。 - 適用: 電商網站、形象官網、需要強大 SEO 的公有產品。
五、 全端架構的狀態管理分流:Client State vs. Server State
過去在後端或簡單前端裡,我們習慣把所有資料塞進同一個大 Store。但在現代 React 架構中,絕對必須把狀態切成兩條完全不同的管線:
- Client State(客戶端狀態):
- 如:Sidebar 是否展開、Modal 開關、主題切換、多語系。
- 推薦工具: Zustand(極簡、無 Boilerplate,寫起來跟 Pinia 有 87 分像)。
- Server State(伺服器狀態 / 遠端資料):
- 如:從資料庫撈出來的使用者清單、商品資料。本質上是「你暫存在前端的遠端快取」。
- 推薦工具: TanStack Query (React Query)。千萬不要把 API 回來的資料手動塞進 Redux 或 Zustand 裡面去維護 Loading、Error、快取過期與自動重試,這些全部交給 TanStack Query 自動化搞定。
六、 架構師級面試高頻考題與滿分回答精選
身為資深工程師或架構師去面試 React 職缺,面試官不會只問死背的語法,而是專攻「底層機制、效能痛點與系統盲點」:
Q1:請解釋 React 的 Re-render 機制?如何避免不必要的重新渲染?
- 核心解析:
- 觸發條件: State 改變、Props 改變、或父元件 Re-render,預設會連帶讓其所有子元件重新執行。
- 優化武器:
React.memo(): 包裹組件進行 Props 淺層比較,避免無辜陪跑。useMemo/useCallback: 快取運算結果與函式參考位址,避免每次 Render 都創立新物件導致子元件誤判。
Q2:為什麼 React 的 State 更新必須是 Immutable(不可變)的?如果直接修改會發生什麼事?
- 核心解析:
- React 是透過記憶體位址的改變(
Object.is比較)來判斷 State 有沒有變。如果直接修改原物件(如state.push),記憶體地址不變,React 會認為「資料沒換」而拒絕觸發 UI 重新渲染。 - 必須透過 Setter 產生全新物件(如
setList(prev => [...prev, newItem])),這也同時確保了資料流的可預測性與時間旅行除錯的可行性。
Q3:useEffect 的依賴項陣列如果沒寫好會出什麼事?請談談「閉包陷阱 (Stale Closure)」。
- 核心解析:
- 當 Effect 內部抓取了某個當下的 State 或 Function,卻漏掉把它放進依賴項陣列時,該函式會因為 JavaScript 閉包特性,永遠「鎖死」在第一次建立時抓到的舊值(Stale State)。
- 解法: 確實補齊依賴項、改用 Functional Update(
setCount(prev => prev + 1)),或透過useRef保存最新數值。
Q4:如果是非同步 API 請求遇上組件卸載(Unmount)或競態條件(Race Conditions),你會怎麼處理?
- 核心解析:
- 若在已銷毀的組件上呼叫
setState會引發記憶體流失警告;若快速切換頁面,慢速 API 蓋掉快速 API 會造成畫面資料錯亂。 - 最佳解法: 優先導入 TanStack Query,它內建了自動取消請求(AbortController)與快取機制,從架構層級根絕此痛點。
🎯 總結:給你的架構師面試加分金句
面試時,可以自信地向面試官表達這套心法:
"雖然過去我們常手動處理useMemo與複雜的生命週期副作用,但在現代 React (如 React 19 / Compiler) 與現代全端架構(如 Next.js + TanStack Query + Zustand)的趨勢下,我們的核心價值在於精準劃分 Server State 與 Client State 的邊界、控制組件的邏輯粒度,並確保資料流的絕對乾淨與可預測性。"
既然是要以資深架構師 / 全端工程師的高規格去應付面試,我們把剛剛的題目升級,補充更多在「中大型專案、團隊協作、架構效能、與底層運作」中最常被電的 7 大進階面試考題(包含面試官的真實心機、考點、與滿分回答策略)。
Q1:請談談 React 19 / 現代 React 的大趨勢中,為什麼大家開始減少使用 useMemo 與 useCallback?什麼時候你還是「非用不可」?
- 面試官想聽什麼: 你是不是只會背古董考題,還是有真正跟上現代 React 生態與 React Compiler(自動化編譯優化)的進展?
- 滿分回答策略:
- 過去的痛點: 以前工程師常常「防禦性優化」,把所有函式都包
useCallback、所有變數都包useMemo,反而因為額外的依賴項陣列檢查帶來效能損耗。 - 現代轉變(React Compiler): 現代 React(配合 React Compiler)已經能在底層自動幫你做記憶化(Memoization),不需要手動到處加。
- 非用不可的時機:
- 當你要把一個函式當作
useEffect的依賴項(避免無限迴圈)。 - 當你要傳遞 Callback 給一個用
React.memo包裹的超大型昂貴子組件,確保其參考位址不變。
Q2:如果在同一個 Event Handler 或非同步事件裡,連續呼叫三次 setCount(count + 1),最終 count 會變成多少?為什麼?如何解決?
- 面試官想聽什麼: 你對 React 批次更新(Batching)機制與狀態非同步特性的理解。
- 滿分回答策略:
- 結果: 最終
count只會加 1,而不是加 3。 - 原因: React 為了效能,會把多個狀態更新進行批次處理(Batching)。在同一個執行脈絡下,讀到的
count永遠是當次 Render 的舊值(也就是快照快取),三次呼叫拿到的都是同一個基礎值去 +1。 - 解法: 使用 Functional Update(函數式更新):
setCount(prev => prev + 1);setCount(prev => prev + 1);這樣每次都會拿到上一次更新後的最新prev,就能順利加 3。
Q3:請說明「Prop Drilling(屬性地獄)」的各種解法?何時該用 Context API?何時絕對不能用 Context API?
- 面試官想聽什麼: 你對狀態範圍(Scope)與效能隱患的權衡能力。
- 滿分回答策略:
- 解法分級:
- 組件組合 (Composition / Children Pattern): 把組件往上提,直接把子組件當作 Prop 傳進去,中介組件不需要知道這顆資料。
- Context API: 適合低頻全域狀態(如:Theme 主題、語系 Locale、目前登入者權限)。
- Zustand / Redux: 適合跨頁面、高頻更新的複雜業務狀態。
- Context API 的致命盲點(絕對不能濫用的原因):Context API 只要裡面的值一變動,所有訂閱(Subscribe)這個 Context 的子組件不管有沒有用到該屬性,全部會強制 Re-render。如果把高頻變動的狀態(例如即時滑鼠座標、倒數計時器)放進 Context,會造成嚴重的整站效能崩潰。
Q4:在大型 React 專案中,如何進行前端效能監控與程式碼分割(Code Splitting)?
- 面試官想聽什麼: 你對首屏載入時間(FCP / LCP)與大型專案架構打包的實戰經驗。
- 滿分回答策略:
- 程式碼分割: 利用
React.lazy()配合Suspense,將不常訪問的頁面或大型重量級組件(例如圖表庫、富文本編輯器)進行動態載入(Dynamic Import),減少初始 JavaScript 包體積。 - 效能指標監控: 關注 Core Web Vitals(如 INP - Interaction to Next Paint、LCP)。避免在主執行緒(Main Thread)塞入過重同步運算,善用
Web Workers或 React 19 的useTransition來處理非阻塞式的高耗能任務。
Q5:如果要你在專案中導入一套全新的 UI Component Library(例如從零打造或選用 Shadcn UI / MUI),基於架構師的角度,你的評估指標有哪些?
- 面試官想聽什麼: 你的工程決策思維(不盲目追求流行,從維護與擴展出發)。
- 滿分回答策略:
- 程式碼所有權(Ownership vs. Dependency): 像 Shadcn UI 這類採用「Copy-paste」機制的組件庫,程式碼直接長在你的專案裡,你可以隨意改寫底層 Tailwind 樣式,沒有版本相容或黑箱死胡同的問題;而傳統 MUI 或是 AntD 則是強依賴外部 NPM 套件,客製化時常需要寫一堆奇怪的 CSS Override。
- Bundle Size(體積影響): 是否支援 Tree-shaking,不會把整包沒用到的元件都打包進去。
- 無障礙支援 (Accessibility / A11y): 底層是否符合 WAI-ARIA 標準(例如 Radix UI / Headless UI 系列在這方面表現極佳)。
- TypeScript 支援度: 型別定義是否完整嚴謹。
Q6:請談談 React Server Components (RSC) 與傳統 SSR (Server-Side Rendering) 的本質差異在哪裡?
- 面試官想聽什麼: 你對現代前端架構分水嶺(Next.js App Router 核心邏輯)的理解深度。
- 滿分回答策略:
- 傳統 SSR: 把整支 React 組件在伺服器跑成 HTML 丟給前端,但瀏覽器收到後還是要把整包 JS 下載下來進行「Hydration(水合作用)」,讓網頁活過來。網頁越大,客戶端 JS 負擔越重。
- React Server Components (RSC):
- Server Component: 跑在伺服器上(可直接連 DB、讀本地檔案),跑完直接變成序列化資料傳給前端,完全不佔客戶端 JavaScript 體積(Bundle 歸零)。
- Client Component (
'use client'): 只有當你需要用useState、瀏覽器事件、或 Web API 時才宣告。 - 關鍵差異: 傳統 SSR 是「整站都在伺服器渲染完送出」,而 RSC 是「細粒度地拆分哪些組件留在伺服器、哪些留在客戶端執行」。
Q7:架構情境題:如果專案接手時發現前人寫了一個「幾千行、所有 State 跟 API 邏輯全塞在同一個組件」的巨大恐龍組件(God Component),你會如何進行重構?
- 面試官想聽什麼: 你的重構策略、模組化思維、以及是否會造成業務中斷(Safe Refactoring)。
- 滿分回答策略:
- 步驟一:不要一開始就打掉重練。 先寫 E2E 或整合測試(或確保現有功能正常運作),釐清邊界。
- 步驟二:抽離商務邏輯(Custom Hooks)。 把裡面雜亂的
useState和useEffect依照業務職責拆成獨立的 Custom Hooks(例如useUserData、useTableFilter)。 - 步驟三:拆分 UI 呈現(Presentational vs. Container Components 或小組件)。 把大區塊拆成獨立的小組件,利用 Props 向下傳遞。
- 步驟四:導入 Server State 管理。 如果裡面有一堆手動 fetch API 的狀態,改用 TanStack Query 託管,直接把幾百行的非同步邏輯砍掉。
沒有留言:
張貼留言