好,呃...今天我們來聊聊 React。不是那種「什麼是 component」的入門東西,我想聊的是那種...嗯,大家都說是「資深」或「進階」的技巧。但說真的,我自己是覺得,所謂的「進階」不是你多會背幾個 API,或是知道什麼很炫炮的 library,重點其實是「判斷力」。你知道什麼時候該用,什麼時候不該用,還有用了之後可能會踩到什麼坑。這才是價值所在。而不是像清單一樣把十個東西列出來,那沒什麼意思。
很多文章都會給你一個列表,一、二、三、四...看到十。但他們很少告訴你這些技術之間的關聯,或是用了 A 會不會讓 B 更難搞。所以,今天我想用一個比較...嗯,像是自言自語的方式,把這些東西串起來聊聊,聊聊它們背後的「為什麼」,而不只是「怎麼做」。
所以結論是啥?一句話說完
TL;DR:真正拉開差距的不是你會不會用這些技巧,而是你懂不懂在「效能」、「可維護性」和「開發速度」這三個點之間做取捨。沒什麼銀彈,全都是 trade-off。
第一個大魔王:應用程式怎麼變快?
這大概是每個 React project 長大後都會遇到的問題。一開始都很快,使用者一多、功能一加,不知道為什麼,網站就開始變得很...很笨重。這邊有兩組核心武器可以處理這件事。
第一組是針對「載入速度」的,就是使用者第一次打開網站的那個感覺。最經典的就是 Code Splitting。
你想想看,你進去一個大賣場,只是想買瓶牛奶,結果你被迫把整個賣場所有商品,連同冷凍庫裡的冰棒跟園藝區的鏟子,全部都先下載到你的腦袋裡...這不是很蠢嗎?Code Splitting 就是讓你只拿那瓶牛奶就好。
在 React 裡,就是用 React.lazy 跟 Suspense 這對組合技。簡單說,就是告訴 React:「嘿,這個 component 先不用急著載入,等真的要顯示它的時候再說。」然後用 Suspense 告訴它在等待的時候要顯示什麼,比如說一個轉圈圈的 loading 圖示。
import React, { Suspense, lazy } from 'react';
// 原本是 import UserProfile from './UserProfile';
// 現在改成這樣
const UserProfile = lazy(() => import('./UserProfile'));
const AdminDashboard = lazy(() => import('./AdminDashboard'));
function App({ user }) {
return (
<div>
<h1>歡迎回來!</h1>
{/* Suspense 可以包住多個 lazy component */}
<Suspense fallback={<div>讀取中,請稍候...</div>}>
{/* 只有當 user 存在時,才會去下載 UserProfile 的程式碼 */}
{user && <UserProfile />}
{/* 只有管理員才需要載入這個超大的後台面板 */}
{user.isAdmin && <AdminDashboard />}
</Suspense>
</div>
);
}
我自己是覺得,fallback 的體驗很重要。不要只是一個簡單的「讀取中...」,如果可以,做一個跟即將載入的內容長得很像的「骨架屏 (Skeleton Screen)」,使用者的體感會好非常多。
第二個效能議題:怎麼別讓它一直重畫?
好,載入變快了,但用起來還是卡卡的。這通常就是 re-render 的問題。React 有時候...呃,有點太敏感了。一有風吹草動,它就把整個畫面都重新畫一次,即使很多地方根本沒變。這時候就要靠「記憶化 (Memoization)」。
你可以把它想像成一個很會做筆記的學生。老師問一個很難的數學題,他算出來之後就把答案寫在筆記本上。下次老師再問一模一樣的問題,他就不用再算一次,直接翻筆記本就好。
在 React 裡面,這本筆記本就是 React.memo、useMemo 跟 useCallback。
React.memo:用來包住整個 Component。如果傳進來的 props 都沒變,它就直接拿上次畫好的結果出來用,連 render function 都不會跑。useMemo:用來記住「計算結果」。比如說你有個超複雜的運算,或是要對一個超大的陣列做篩選排序,你就可以用useMemo把算出來的結果存起來。useCallback:用來記住「函式本身」。這聽起來有點怪,函式就函式,記住要幹嘛?因為在 JavaScript 裡,每次 re-render,Component 裡面的函式都會被「重新創建」一次,即使長得一模一樣,但在記憶體裡是不同的人。如果你把這個函式當成 prop 傳給一個有用React.memo包住的子元件,那子元件就會覺得「欸,prop 變了!」,然後就破功,又 re-render 了。useCallback就是為了解決這個問題。
不過呢,這東西是雙面刃。千萬、千萬不要沒事就把所有東西都包一層 memo。因為做筆記本身也是要花時間花力氣的(佔用記憶體、做比較),如果你記的都是一些無關緊要的小事,反而更浪費資源。只有在「props 真的很少變動」而且「元件本身 re-render 很昂貴」的時候,用它才有意義。
讓程式碼變乾淨的魔法:Custom Hooks
OK,效能搞定了,再來是程式碼的「可維護性」。我覺得這點是區分中手跟高手的關鍵。你的程式碼是不是幾個月後你自己還看得懂?別人接手會不會想罵髒話?
Custom Hooks 絕對是 React 史上最偉大的發明之一,沒有之一。它解決了一個長久以來的痛點:怎麼在不同的 Component 之間共享「有狀態的邏輯 (stateful logic)」。以前我們要用 HOC (Higher-Order Components) 或 Render Props,那寫起來真的是...嗯,很痛苦,會產生所謂的「Wrapper Hell」,一層包一層,像俄羅斯娃娃一樣。
Custom Hook 說穿了,就是一個函式,它的名字必須是 `use` 開頭,然後它裡面可以呼叫其他的 Hooks,像是 `useState`、`useEffect`。就這樣。
比如說,你很多頁面都需要去 call API 拿資料,然後處理 loading 跟 error 狀態。你就可以把這整套邏輯抽出來,變成一個 useFetch 的 hook。
import { useState, useEffect } from 'react';
// 這就是一個 Custom Hook
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// 每次 url 變了,就重新抓一次資料
setLoading(true);
setError(null);
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
setError(error);
setLoading(false);
});
}, [url]); // 依賴 url,當 url 改變時,這個 effect 會重新執行
// 把狀態跟結果回傳出去
return { data, loading, error };
}
// 在你的 Component 裡就可以這樣用,超乾淨
function MyUserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <p>讀取中...</p>;
if (error) return <p>糟糕,出錯了:{error.message}</p>;
return <div>使用者名稱:{user.name}</div>;
}
你看,Component 裡面變得超乾淨,它只關心「拿到資料要幹嘛」,完全不用管「怎麼拿到資料」的那些髒活。那些髒活全都封裝在 useFetch 裡面了。你可以到處重複使用這個 hook,超實用。
最頭痛的問題:狀態管理到底用哪個?
這大概是 React 社群吵最兇的議題了。當你的應用程式變複雜,元件之間的層級變深,單純靠 props 把資料傳下去會變成一場災難,就是所謂的 "Prop Drilling"。想像一下,你要把一個資料從阿公傳給曾孫,中間要經過爸爸、叔叔、伯伯...每個人都要經手一次,超煩。
為了解決這個問題,就出現了各種狀態管理的方案。官方有 Context API,社群有 Redux、Zustand、Jotai...等等一大堆。到底該怎麼選?
老實說,沒有標準答案。但我可以給你一個我自己判斷的思路,我把它整理成一個表格。
| 解決方案 | 適合情境 | 優點 | 缺點(或說...要注意的地方) |
|---|---|---|---|
| Prop Drilling (硬傳) | 超簡單的 App,元件層級不超過 2-3 層。 | 最直覺,不用學新東西,程式碼流向很清楚。 | 元件一多、層級一深,馬上變義大利麵,改個東西要改一整串。 |
| Context API | 中小型 App,需要共享的狀態不多,例如:主題 (Theme)、使用者登入狀態。 | 官方內建,不用裝東西,API 也算簡單。 | 這是大坑:只要 Context 的 value 一變,所有用到這個 Context 的元件「全部」都會 re-render,不管它是不是真的需要那個變動的部分。很容易有效能問題。 |
| Zustand / Jotai (原子化狀態) | 從中型到大型 App 都很適合。我自己現在的新專案幾乎都用 Zustand。 | 寫起來像 Hook,學習曲線低。可以做到只更新「有訂閱」該狀態片段的元件,效能好很多。不用像 Redux 那樣寫一堆 boilerplate code。 | 畢竟是第三方庫,需要額外安裝。團隊成員需要一點時間習慣它的思維模式。 |
| Redux (+ Redux Toolkit) | 超大型、超複雜的企業級應用。狀態變更有嚴格的追蹤需求。 | 狀態流向超級嚴謹、可預測,搭配 Redux DevTools 根本是除錯神器。生態系非常成熟。 | 寫起來很囉嗦,概念也比較繞。為了改一個小狀態,要寫 action, reducer... 新手會覺得很挫折。現在有了 Redux Toolkit 已經好很多了,但還是比 Zustand 複雜。 |
所以你看,沒有哪個是最好的,只有最適合的。對於大多數專案,我會說從 Context 開始,如果發現效能瓶頸了,或是狀態變得太複雜,再考慮遷移到 Zustand。Redux...說真的,除非你的專案規模跟 Facebook 一樣大,不然可能有點殺雞用牛刀了。
還有一些雖然不常用,但關鍵時刻能救命的技巧
除了上面那些大主題,還有幾個小技巧,平常可能用不到,但一出事的時候,你會很感謝自己知道這些東西。
第一個是 Error Boundaries。你的 App 再怎麼測,上線後就是有可能會壞掉。最慘的情況是,某個小元件一壞,整個 App 白畫面給使用者看。Error Boundaries 就是一個「保險絲」,你可以用它包住你的元件,如果裡面的元件在 render 的時候出錯了,它會接住這個錯誤,然後顯示一個你預先準備好的「壞掉畫面」,而不是讓整個 App 崩潰。
不過要注意,根據 React 官方文件(這就是我說的非本地權威來源),它只能抓到 render 階段的錯誤。像是在 `useEffect` 裡的非同步錯誤、或是在 event handler(比如 `onClick`)裡的錯誤,它是抓不到的。那些你還是得用傳統的 `try...catch` 去處理。
第二個是 Portals。這東西超酷。一般來說,一個 Component 的 HTML 最終會被 render 到它在 DOM tree 裡的位置。但有時候,你希望把這個 Component 的 HTML「丟」到頁面的另一個地方去。最常見的例子就是 Modal (彈出視窗) 或 Tooltip (提示框)。你希望 Modal 的 HTML 結構是直接掛在 `body` 底下,這樣才不會被父層的 `z-index` 或 `overflow: hidden` 之類的 CSS 搞到,但它的邏輯跟狀態又明明是屬於某個子元件的。Portals 就是做這件事的橋樑。
最後一個,Server-Side Rendering (SSR)。這已經不只是 React 的範疇了,通常會搭配像 Next.js 這樣的框架來做。簡單說,傳統的 React App (CSR, Client-Side Rendering) 送到瀏覽器的是一個空的 HTML 跟一包很大的 JavaScript,瀏覽器要下載、執行完 JS 才能把畫面畫出來。這對 SEO 很不友善,對使用者來說也可能要看一段時間的白畫面。SSR 則是在 Server 端就先用 React 把頁面 render 成完整的 HTML 字串,再送給瀏覽器。這樣使用者可以馬上看到內容,搜尋引擎也看得很開心。這點在台灣的電商網站尤其重要,像是 PChome 或 momo,他們的首頁載入速度跟 SEO 排名是生死存亡的關鍵,所以他們很多頁面都會用上類似 SSR 或 SSG (Static Site Generation) 的技術,這跟國外很多純粹做後台系統的 SaaS 產品的架構思路就很不一樣。
那...我該從哪一個開始學?
如果看到這邊你覺得有點資訊爆炸,很正常。我自己的建議是,你不用一次全會。你可以根據你現在專案的痛點來決定優先級。
- 網站載入慢? → 先去研究
React.lazy和 Code Splitting。 - 用起來卡卡的? → 打開
React DevTools裡的 Profiler,看看是哪個元件在鬼打牆一直 re-render,然後試試看用React.memo。 - 程式碼亂成一團,不敢改? → 這絕對是引入 Custom Hooks 的最佳時機。把你重複的邏輯抽出來。
- Props 傳到天荒地老? → 該是時候導入狀態管理了。從 Context API 小試身手,或直接勇敢挑戰 Zustand。
說到底,這些工具都只是為了解決問題。重點是先定義出「問題」到底是什麼。這就是我一開始說的「判斷力」。一個資深開發者的價值,不是他會背誦多少 API,而是他能準確診斷問題,然後從他的工具箱裡,拿出最適合的那一把扳手。
好啦,今天大概就先聊到這。不知道我這樣東拉西扯的,對你有沒有幫助。如果你現在只能選一個技巧去深入研究三個月,你會選哪個?Code Splitting?還是狀態管理?在下面留言聊聊吧,我還蠻好奇大家現在都卡在哪個關卡。
