React useState、useEffect 與 useReducer 差異解析:狀態管理與效能優化的選擇點

Published on: | Last updated:

讓 React Hook 真正提升你的開發效率與效能,不再瞎猜該用哪個

  1. 狀態超過 3 個互動欄位時直接改用 useReducer,別再用一堆 useState 疊起來

    複雜表單或購物車這種多步驟流程,reducer 能讓你一眼看懂所有狀態變化邏輯,debug 時間省一半以上(開 React DevTools 看 state 更新次數,從原本 8-10 次降到 2-3 次就知道有效)[1]

  2. useEffect 裡如果要跑運算或 API call,先問自己「這真的需要每次 render 都檢查嗎」,不需要就搬到事件處理函式

    很多人把 onClick 該做的事丟進 useEffect 依賴陣列,結果畫面一動就觸發,白白浪費效能…改成按鈕直接呼叫函式後,你會發現頁面滑動變順、手機測試時也不再有卡頓感

  3. 列表渲染超過 50 筆資料時,把過濾或排序邏輯包進 useMemo,dependency 只放真正會變的變數

    每次 render 都重算 filter() 或 sort() 超耗資源,加了 useMemo 之後在 Chrome DevTools Profiler 裡看 Render Duration 能從 120ms 降到 15ms 以下,用戶操作立刻有反應[1]

  4. useCallback 只在把函式當 props 傳給 memo 過的子元件時才用,其他情況別亂加

    盲目包一堆 useCallback 反而讓 React 要額外比對依賴,記憶體也多佔空間。簡單判斷:子元件沒被 React.memo 包起來?那父層的 useCallback 根本沒用,刪掉後 bundle size 還能少幾 KB

  5. 搜尋框或即時篩選這種高頻輸入場景,試著用 useDeferredValue 延遲更新大清單,保持輸入框本身絲滑

    打字時畫面頓一下超惱人,用 useDeferredValue 把搜尋結果更新往後推 100-200ms,輸入體驗瞬間變流暢(手機上最明顯,Lighthouse Performance 分數能從 60 拉到 85+)

了解 React useState 與 useEffect 常見問題點

React 的 useState 跟 useEffect…這兩個,唉,其實很難不認識啦,如果有在寫 React,通常每天都會碰到,就是那種 - 打開編輯器你可能第一個想到就要加進來的鉤子。處理狀態跟副作用,幾乎所有教學一開始也是拉著它們猛講。其實,有時候會覺得沒啥新意,都被說到爛了對吧?我猜你也是差不多。

嗯,但回頭看看,好像大家一直都只黏著 useState 和 useEffect 不放…比較少注意那些「其他」hooks。有點奇怪,其實官方文件寫得很清楚啊,都有列出來,也算是標準配備,只是手上老用習慣的那幾招,要伸去試別的,有時候就是懶一下這樣…我自己也常這樣卡住。

突然在想,要不要讓你的 React 開發再滑順一點?以前覺得有瓶頸、好像哪裡可以精簡流程,可是腦袋裡的解法又總卡同一招。其實那些不太起眼的小 hook,有些真的是—嗯,可以節省滿多時間,也常常讓程式跑更快。不誇張,有種偶然翻到舊抽屜還能撈出藏寶感。如果你稍微理解他們各自厲害在哪裡、什麼情境下超適合出場,以後寫東西省事超多 - 真的不是騙人的那種輕鬆,而是一用就回不了頭。

反正,我等等帶一下自己覺得比較酷、平常大概沒特別研究過的小玩意兒。不保證全部人都有共鳴啦,但至少,下次寫 code 時,你手邊可選的不止之前學到的那兩套。

找出什麼情境下優先採用 useReducer 處理複雜狀態

嗯...這個用 React 寫前端,講真的用 useState、useEffect 啊,不是每次都能隨心所欲。尤其是 useEffect,很容易一不小心就踩到坑,你知道那種 debug 到懷疑人生的感覺嗎?

像 useEffect 最常的就是...大家習慣只要有副作用就往裡面丟,比如資料拉取啦、訂閱外部事件啦、或者直接去改 DOM,那個 array 裡面依賴只要沒填好,每次 re-render 可能又一直執行,畫面像跳針。然後 race condition,我自己超討厭,就是你同時發送幾個請求,結果回來順序跟預期不同,你還得想辦法打補丁。再有一種更煩的是,有一堆 useEffect 結果互相 chain 在一起,一改某值下游全部動起來,bug 根本抓到崩潰。

舉個例子(我自己之前踩過):  
useEffect(() => {
const fetchData = async () => {
const response = await api.fetchSomething(id);
setData(response); // 萬一組件還沒卸載?那資料設下去也怪怪的啊
};
fetchData();
// 沒寫 cleanup 的話,記憶體直接開始飛了~
}, [id]);


另外 useState,看起來無腦,其實真的遇到狀態多、欄位彼此會連動的時候,那程式嘛... 一團亂。如果做表單,多步驟那種,你每次改 A 要記得同步 B 跟 C,不然一下搞壞全局。有時你真的漏掉某欄更新,下場基本就是畫面爆錯(尤其多步驟跳轉回去還原狀態那邊)。

題外話,如果你卡在這些地方,也別太挫。其實官方和社群早推不少新 hook 方案,比方說 Reducer 或專門處理 context 的,好用很多。不只是可以把程式整理乾淨,有時性能上也輕鬆些。我蠻期待有機會再聊那些“進階版”的 Hook,到底什麼狀況下派上用場—說真的學起來很開心,用對真的是會省掉不少麻煩!

找出什麼情境下優先採用 useReducer 處理複雜狀態

判斷何時使用 useMemo 及 useCallback 強化效能

你有沒有碰過那種狀況,就是 state 超複雜、超煩,然後用 useState 會一直 set 來 set 去,每個地方都要顧一下,好像哪邊沒顧到資料就整個亂掉?我自己很常遇到啦。不過說真的,這時候換成 useReducer,那感覺完全不一樣。它就是乾脆把所有動作和更新規則全部塞進同一支 function 裡,那支 function 我們都叫 reducer。操作起來其實滿像在管理一堆分岔小路,但全部的地圖你都集中在手上這樣。

想到一個例子。有看到別人在寫購物車,他就寫了一個 cartReducer,把 'ADD_ITEM'、'REMOVE_ITEM'、'CLEAR_CART' 分開處理。每當需要變更狀態,只要呼叫 dispatch({ type: xxx, payload: yyy }) 就能走固定套路,也不用怕資料更新流程被搞爛。然後他的 state 就是 items 跟 total 金額,每次商品加減規則只寫一次,很統一。我覺得這裡外面元件完全不用操心怎麼計算價錢或重組陣列啊,都交給 reducer 處理就對了。

所以像 ShoppingCart 那元件,一開始直接用 useReducer 搭配 cartReducer,然後預設 { items: [], total: 0 } 當起手式,比如 addToCart 的時候就是 dispatch 一下 { type: 'ADD_ITEM', payload: product } 超順;有移除按鈕也是 dispatch({ type:'REMOVE_ITEM', payload:id });清空購物車也很直觀,就丟 'CLEAR_CART' 而已。我看他們架構出來簡單又明瞭,不太會發生奇怪的小錯誤。

最重要的是:反正所有涉及 state 改變的規則,全放 reducer 內搞定,以後哪天突然需求大改,加折扣碼、多幣別什麼的,只要改 reducer 不用去地毯式搜尋那些奇怪的 setXX 再改回來。而且如果遇到比較麻煩或複雜點的需求 - 譬如多步驟表單流程啦,或是有很多細微欄位聯動的東西 - 真的還蠻推薦想一下是不是可以拉 useReducer 出來救場。

還有性能最佳化這段…唔,其實關於 useMemo 和 useCallback 很多朋友可能聽過,有些人會拼命加,可是老實講用太兇反而慢掉也是可能發生哦。所以不能亂灑,很妙吧。

體驗用 useTransition 改善 React UI 流暢度

好啦其實你知道 useMemo 跟 useCallback 這兩個東西,真的要說什麼時候有差,其實超直觀,就是看你在算東西到底花不花力氣。你平常做一個什麼產品列表的頁面,如果只是簡單秀幾個資料,其實怎麼重新 render 都沒啥感覺啦,完全不用理它;但如果你今天搞的是那種上千條產品然後還要過濾啊搜尋什麼很複雜的邏輯,就會變得「沒有記一下」會很笨重,例如像這樣寫:

// 用 useMemo 記住複雜又重的運算結果
function ProductList({ products, filter }) {
const filteredProducts = useMemo(() => {
console.log("Filtering products...");
return products.filter(product =>
product.name.toLowerCase().includes(filter.toLowerCase())
);
}, [products, filter]);


return (
<ul>
{filteredProducts.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}


意思就是,你 products 或 filter 改了才重算,不然每次重新渲染都直接用 cache,很省電腦力氣也快很多。不記的話,如果資料多爆炸會 lag 到哭。

換成 useCallback 的情境,也很生活化。基本上只要你把某個 function(例如事件 handler)丟給子組件,而小孩剛好又加了 React.memo 去防止無謂重渲染,那 function 每次都產生新身分證,小孩看到就忍不住再 render 一下。有點像…爸爸講同一句話但換了一張臉,小朋友就以為一切都不同了:

// 用 useCallback 綁定住 function reference
function ParentComponent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log("Button clicked!");
}, []);


return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<ChildComponent onClick={handleClick} />
</div>
);
}


// 小孩有 memo 就不亂動
const ChildComponent = React.memo(({ onClick }) => {
console.log("Child rendered");
return <button onClick={onClick}>Click me</button>;
});


老實說,大部分時候根本用不到這些花招,React 本來就蠻快,但遇到真的「卡」或效能疑慮場景才值得搬出來。useMemo 處理費工的計算、useCallback 幫 events 固定 reference,小孩不會白跑。

對了順便講下最近比較夯的 React 新功能,就是 React 18 那個 useTransition。我第一次看到時超級驚訝,因為可以讓你的畫面不卡頓 - 原理大致上是,它可以標註哪些 state 更新屬於「transition」,反正就是可能會拖慢速度、但是用戶操作又不能斷掉的東西(比如搜尋資料、大量篩選那種)。聽起來太抽象?來,看例子最實際:

function SearchComponent() {
const [query, setQuery] = useState('');
const [searchResults, setSearchResults] = useState([]);
const [isPending, startTransition] = useTransition();


// 用戶打字即時回饋、重的事情交給 transition 處理
const handleSearch = (e) => {
const newQuery = e.target.value;

// input 即刻更新,不等任何結果
setQuery(newQuery);

// 大量資料處理丟進 transition,頁面完全不卡!
startTransition(() => {
const results = performExpensiveSearch(newQuery, largeDataset);
setSearchResults(results);
});
};

return (



<pre><code class="language-html"><pre><code class="language-html">{isPending ? (
<p>Searching...</p>
) : (
<ul>
{searchResults.map(result => (
<li key={result.id}>{result.name}</li>
))}
</ul>
)}
</div>
);
}


function performExpensiveSearch(query, dataset) {
if (!query) return [];

return dataset.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
);
}

const largeDataset = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
description: `Description for item ${i}`

運用 useDeferredValue 解決重資料渲染延遲困擾

你知道嗎,`useDeferredValue` 這個 hook 根本是為了資料視覺化量身打造的!像在畫一些圖表啊、那種超級長的表格,每次打字如果都要即時顯示真的很容易卡住,不太需要死守「輸入框一改畫面就跟著動」這種同步(很寫實吧,其實很多場合根本不用那麼緊)。然後你就可以一路狂打字,也不用怕整個 App 卡在那邊等計算,因為 React 會先用舊資料閃一下,等到真的重算好才換新結果。

對了講個 DataVisualization 例子,它開頭 filter 用的就是使用者正在輸入的內容,但拿來真的過濾用的是 deferredFilter,那個會慢一點點。重點就是讓 input 欄位永遠順滑不卡,不管下面資料篩選或 ExpensiveChart 重渲染多久,都沒關係。欸只要偵測到現在 filter 跟 deferredFilter 有差別,還會主動丟一個 Loading... 給你看(這細節蠻可愛的),就算有延遲你也知道狀況。

ExpensiveChart 這名字取得直白吧?它設計就是硬要裝得很慢:直接來個 while 迴圈睡五十毫秒,故意卡,你馬上能比較出有 useDeferredValue 跟沒用時體驗差多少。如果你的 chart 很複雜、資料超多,那效果會更明顯,用戶操作也不會被拖累 - 順得不得了!

再來換 `useSyncExternalStore`,這東西主要解決 React 想黏外部狀態又頭痛的問題。不是只有 useState 或 Context 才能掌控元件狀態嘛,有時候 state 其實藏在 localStorage、Redux 或甚至 Zustand 這些外面系統裡,`useSyncExternalStore` 就是專門幫你把外部 state 接回 React 的工具人。

直接舉例好了,LocalStorageComponent 就是典型範本。它整個就是靠 useSyncExternalStore 訂閱 localStorage,只要有誰更新 localStorage - 例如瀏覽器主動觸發 storage event - React component 就會立刻抓最新值再 render 一次。有些地方(像主題切換按鈕)也會直接搞 storage event,就是為了讓所有訂閱同一組 key 的元件馬上跟上變化。

這 hook 到底什麼情境最有感?如果你習慣整合 Redux、Zustand 那類外部 state library,本來就大推;但日常常常自己和 localStorage 或 IndexedDB 打交道,其實也是神隊友啦!無論大工程還是小 side project,只要碰到「React 控不到但一定要和別人同步」那種微妙尷尬場景,就直接塞 useSyncExternalStore 下去,用起來絕對療癒。

利用 useSyncExternalStore 串接本地與外部同步狀態

React 的 useId,這個 Hook,其實我真的蠻常用,因為你知道嗎,它就是很方便啊!可以直接在 server 跟 client 兩邊,產出一模一樣的 ID,不用怕會撞名。想像一下你有一堆輸入框,然後每個 label 都要對到對應的 input,htmlFor 屬性嘛,用 useId 真的超快,每次都自動配對,而且 SSR 下也完全沒在怕 hydration 亂掉。

欸來講個實際例子。如果你做註冊表單,有名字、email、密碼好了,根本不用手動去搞什麼亂碼或隨機數字。你直接 const nameId = useId()、emailId = useId()、passwordId = useId(),真的一行寫完。然後 label 的 htmlFor 用那個 id,input 的 id 就直接對上,啊對,這樣對無障礙設計超有幫助,不用再去寫那種 getRandomId() 之類自己土炮出來的 function,完全省事。

說到表單狀態管理,其實如果欄位少還行啦,但你有沒有遇過表單一長起來,每個都要 setState?像 firstName、lastName、email、password 什麼的一堆,errors 還要分開,每次欄位更新就 trigger render,一頁一直重跑 isValid,然後什麼 derived state 狀態也全都跟著動,超級沒效率的。老實說我自己遇到這種時候都超煩躁。

然後 useReducer 超適合這種場景。一開始就把所有欄位的初始值塞進一個 values 物件,然後用 reducer 來控制 field 更新、驗證或錯誤處理。比方說 UPDATE_FIELD action 一次就能更新值順便清錯誤訊息,VALIDATE action 就整包檢查未填寫的欄位,把錯誤一次歸位,然後 isValid 一起設定。這種狀態全部集中起來超爽的啦,看 code 一眼就懂邏輯怎麼跑。

還有很重要的一點是,用 useReducer 只有 dispatch 的時候才 batch update,不像 useState 每個 input 動一下就重 render,那種閃爍真的很煩。有一招是寫 handleChange 工廠函數,給 field name 一直傳下去,每個 input 都能共用那支 handler。然後送出表單時只 dispatch VALIDATE,一次就搞定驗證,也不會多頭馬車。

這種組法好處滿明顯。第一就是狀態怎麼變化全部在 reducer 裡超清楚;第二欄位多也能合併更新,不會動不動就重 render 整頁;第三錯誤跟驗證都一起統一處理,很快發現 bug 或修邏輯。反正啦,如果你有遇到表單很大很複雜,只用 useState 管真的會瘋掉,改用 useReducer 把 state 都抓在一起,隨便長的表單也很好維護,用起來順暢感覺差蠻多的。

利用 useSyncExternalStore 串接本地與外部同步狀態

創建無衝突且 SEO 友善的 ID 利用 useId 實現

用 useTransition,其實還蠻適合那種資料量一多就很卡的 React 場景。像你真的有沒有試過商品列表搜尋,沒有加 useTransition,欸,畫面直接不動,等半天超煩的。ProductListWithoutTransition 的做法,關鍵字每打一下,馬上整包 filter 跑一次,資料又一大堆,直接 UI 卡住,連反應都沒有,有時候甚至會跳出那個「網頁未回應」的小視窗... 真的讓人很想直接重開分頁。

他們寫的那個是把 filter 跟 displayedProducts 都丟進 useState,然後 handleFilterChange 每動一下 input 就觸發。可是 products.filter 資料如果又特別肥,結果就是畫面全死光,什麼體驗都沒有。老實說這種遇到瀏覽器直接說要關掉的那種情形,真的很惱人。

然後 ProductListWithTransition 這邊做法完全不一樣。有再多加 useTransition,那你在改 input 的時候,setFilter 會先變動,打字馬上看到結果(就是那種打一下立刻跟上你),重的 filter 計算再包進 startTransition 慢慢處理。所以操作超順,有一個 isPending 在跑「Updating results...」,不會突然整塊都卡住,看起來 UI 至少還有動作,也比較有安全感吧。

順便提一下,不是每件事都需要搞這麼花。不用說每次動一點小狀態都拉 useReducer,那像切 switch、單純計數器、普通 input,老老實實用 useState 就搞定,不要平白無故讓邏輯變更複雜。那種硬是想每招都拆,反而自己困在裡面。

還有,useMemo、useCallback 超多人濫用,一堆地方都塞,其實那兩個也是要成本的,不見得加越多越好。有需要的時候才用,比如說真的是重複算同一堆東西,或者 function 每 render 都重新生出來怕子元件被觸發之類的,再考慮用。不然只是把 React 弄成黑箱,到最後 debug 起來超累。

比較傳統 useState 與進階 hooks 下的表單效能差異

其實,先不用急著搞那些優化技巧。真的要等有明顯的效能問題再說吧。大部分情況下,Modern React 已經很順了,卡頓?老實說,我自己好像也不常遇到。問到 useTransition 什麼時候用?如果你那個狀態本來就更新得很快,坦白講,就不要加。反而程式會變更亂,然後執行根本沒差到哪裡去。

比較傳統 useState 與進階 hooks 下的表單效能差異

發現如何避免不必要 hooks 濫用以減少性能浪費

今天要來聊聊一個超容易搞混的點 - React 裡面的 useSyncExternalStore。這個 hook 很容易讓人誤用,尤其是你在自己管理 state 的時候,其實根本不應該碰它。它就是設計來處理「外部資料源」的,比如那種不是 React 管的資料,如果你把它拿來自己 state 用,嘿,那真的很容易出現很怪的 bug,你真的會後悔(我就踩過)。別自找麻煩了。

說到 React 這些「看起來冷門但很強」的 hook,其實還有一堆很有趣的玩法,不要只記得 useState 或 useEffect。複雜狀態?直接上 useReducer,一開始可能覺得難,但其實理順之後爽度很高。有性能瓶頸?useMemo 跟 useCallback 搭起來超有效。然後要 UI 更順滑,用 useTransition、useDeferredValue,那感覺真的差很多。外部狀態?這時才去用 useSyncExternalStore,不要搞錯時機。

對了,useId 這個也不能漏講,特別是在做無障礙功能或是 SSR 時超級重要,它能幫你省下很多 id 重複頭痛問題。說真的,我覺得 React 的 hooks 最帥的地方,就是可以各種搭著玩,自由組合出屬於你專案的寫法。不用當作什麼教條規則啦,就像工具箱一樣,你覺得哪個好用就抓來配,用舒服最重要。

嘗試多種 hook 組合讓 React 開發更輕鬆高效

遇到那些很麻煩的狀態或效能的時候 - 嗯,講真,不用太慌啦。大部分情況下React早就有對應的hook等著你,只是,有時候會漏掉一個名字,或者沒注意到,看起來就很煩,但結果只是還沒找到那個剛好能幫你的hook而已。

唉,我自己以前也是這樣,常常看到一大堆「不紅但超實用」的小hook,不知道要怎麼下手。其實不用想太多啦,直接拉進自己的專案玩一下就行了。你會發現,那些看上去複雜得要命的東西,其實只要抓對那顆hook,就突然變得很單純欸。我一直覺得hooks比我最開始想像的還靈活多了,也比較有趣。

然後如果說真的哪邊卡住懷疑人生…官方那份Hooks Reference可以先翻一下,它裡面羅列很多嘛,而且寫法就是直接範例給你。有興趣特定功能,比方說useState、或者新出現像React 18的useTransition,都有人整理過操作方式;memoization相關也是資料滿天飛啦。我自己不是每次都馬上看懂,可是慢慢查一查真的什麼都有。別急着一次全記下來,就當在倉庫裡找工具,多摸幾次自然順了。

Related to this topic:

Comments