跳至主要内容

課程:Redux 思維與 RTK 核心 第 1 堂:Redux 思維建立

02Redux 三大原則

如果你曾經在大型 React 專案中迷失過,你一定體會過那種「狀態到底在哪裡被改掉」的絕望感。在上一部分,我們探討了狀態管理的痛點,特別是 Props Drilling 和 Context API 的侷限。你可能會問:Redux 既然是為了應對這些問題而生,為什麼它看起來這麼「嚴格」?為什麼它強迫我們寫這麼多看似繁瑣的程式碼?

答案只有一個詞:可預測性(Predictability)

軟體工程中有一句名言:「自由通常伴隨著混亂。」Redux 選擇了另一條路——它透過設定極其嚴格的「限制」,換取了絕對的可預測性。當你遵循這些規則時,你的應用程式狀態變遷將變得像數學公式一樣精確,無論專案變得多麼龐大,你都能清楚地知道每一格狀態是「誰、在什麼時候、因為什麼原因」而改變的。

這套限制被濃縮成著名的「Redux 三大原則」。在我們動手寫 Redux Toolkit(RTK)程式碼之前,必須先內化這三大心法,因為 RTK 的所有設計,本質上都是為了讓我們更輕鬆地遵守這三條鐵律。

單一資料來源:Single source of truth

在沒有 Redux 的世界裡,狀態分散在各個元件的 useStateuseRef 中。當你需要同步兩個相距甚遠的元件狀態時,你必須不斷地把狀態提升(Lifting State Up),最終導致整個 App 的根部掛滿了無數的狀態。

Redux 的第一條原則是:整個應用程式的全域狀態(Global State),都應該被儲存在唯一一個 Store 內部的物件樹(Object Tree)中。

為什麼要「單一」?

想像一下,如果你正在開發一個電商平台,購物車的資訊同時存在於導覽列(Navbar)的狀態中,也存在於商品列表(ProductList)的狀態中。如果這兩個地方的資料不一致(例如 Navbar 顯示 3 件,列表顯示 2 件),這就是所謂的「狀態不同步」。

當我們把所有東西都塞進同一個「單一 Store」時,我們可以獲得以下幾個巨大的優勢:

  1. 除錯的聖杯(Debugging):因為所有的狀態都在同一個地方,你可以隨時打開 Redux DevTools 看到整個應用程式的完整快照。不需要去翻找數十個元件的內部 State,整個應用的當前模樣一目了然。
  2. 伺服器端渲染(SSR):在做 SSR 時,伺服器只需要準備好這個「單一物件」,並將它序列化(Serialize)成字串傳給前端。前端接收到後,可以直接把這個物件注入 Store,達成「狀態脫水與復水」(Hydration),確保前後端狀態完全一致。
  3. 持久化與恢復(Persistency):想要把使用者的草稿保存到 LocalStorage?在單一 Store 的架構下,你只需要把這個大物件存起來,下次打開時讀取回來即可,不需要分別處理幾十個不同的狀態片段。

這並不代表不能使用 useState

這是一個常見的誤區。身為進階開發者,你必須意識到:單一資料來源是指「全域狀態」。如果一個狀態只與某個元件的 UI 表現有關(例如:選單是否展開、表單輸入中的暫時字串、分頁元件的當前頁碼),它依然可以留在元件內部的 useState 中。Redux 的目的是管理那些「多個元件共享」或「業務逻辑核心」的資料。

狀態是唯讀的:State is read-only

這是初學者最容易犯錯的地方,也是最違背直覺的地方。在 Redux 中,你絕對不能直接修改 state 變數。

如果你想改變狀態,唯一的方法就是發送(dispatch)一個 Action,這個 Action 是一個用來描述「發生了什麼事」的純 JavaScript 物件。

為什麼不能直接修改(Mutate)?

假設你有一個待辦事項列表:

// ❌ 絕對錯誤的做法
state.todos[0].completed = true;

如果你直接這樣寫,雖然 JavaScript 物件的值變了,但 Redux(以及 React)並不知道它變了。React 是基於「比較」來決定是否重新渲染的。如果你修改了同一個物件的內部屬性,物件的記憶體引用地址(Reference)沒有改變,React 會認為「喔,還是同一個物件,沒事」,進而導致 UI 不會更新。

更重要的是,「直接修改」會破壞追蹤能力。如果應用程式中任何地方都能隨意修改全域變數,當 Bug 發生時,你根本無從得知是哪個元件、在什麼時間點把資料改壞了。

透過 Action 表達意圖

在 Redux 的世界裡,狀態變更是「莊嚴且正式」的。你必須發送一個 Action:

// ✅ 正確的做法:發送一個 Action 物件
dispatch({
type: 'todos/todoCompleted',
payload: { id: 1 }
});

這種限制帶來了可追蹤性。在 Redux DevTools 中,你會看到一連串按時間排序的 Action。你可以清楚看到,狀態是先執行了 todoAdded,然後執行了 todoCompleted。Action 就像是應用的「操作日誌」,讓所有的狀態變更都變得透明且可預測。

補充:雖然我們之後學 RTK 時,看起來可以在 *createSlice* 裡面寫 *state.value = 1* 這種「直接修改」的語法,但那其實是 RTK 內建了 *Immer* 庫幫我們轉化成了不可變的操作。底層邏輯依然嚴格遵守著「State is read-only」的原則。

使用純函數進行變更:Changes are made with pure functions

既然我們不能直接修改 state,也不能直接操作 Store 來改變資料,那狀態到底是怎麼更新的?

這就是 Reducer 的守備範圍。為了定義 Action 如何轉換 State,你需要撰寫 Reducer。而這第三條原則規定:Reducer 必須是純函數(Pure Function)

什麼是純函數?

一個函數要被稱為「純函數」,必須滿足兩個條件:

  1. 確定性(Deterministic):給定相同的輸入(參數),永遠會得到相同的輸出。
  2. 無副作用(No Side Effects):函數執行過程中不會修改外部變數、不會發送 API 請求、不會寫入資料庫、不會進行 DOM 操作。

Reducer 的函式簽名通常長這樣: (previousState, action) => newState

它接收「舊的狀態」和「發生的 Action」,然後計算出並回傳「新的狀態」。它不應該修改 previousState,而是回傳一個全新的物件。

為什麼一定要是純函數?

這不是為了炫耀函數式編程(Functional Programming)的技巧,而是有非常實際的開發效益:

  1. 時間旅行除錯(Time Travel Debugging): 如果你每次更新狀態都是回傳一個全新的物件,那麼 Redux 就可以保存所有歷史狀態的快照。因為 Reducer 是純函數且不具副作用,Redux 可以隨時「重播」或「跳回」到任何一個歷史時間點,而不用擔心會觸發不必要的 API 呼叫或奇怪的副作用。
  2. 測試極其簡單: 你不需要模擬(Mock)任何瀏覽器環境或網路請求。你只需要給 Reducer 一個舊狀態和一個 Action,檢查回傳的物件是否符合預期。這使得 Redux 的邏輯測試變得異常穩定且快速。
  3. 預測性與一致性: 如果 Reducer 內部使用了 Math.random()new Date(),那麼同一個 Action 在不同的時間點執行,產生的結果就會不同。這會徹底破壞我們追求的「可預測性」。

絕對不能在 Reducer 做的事

在撰寫 Reducer 時,請務必像強迫症一樣檢查:

  • 不可 修改傳入的參數(state)。
  • 不可 執行有副作用的操作(如 API 請求或路由跳轉)。
  • 不可 調用不純的函數(如 Date.now()Math.random())。

總結:三大原則如何共同運作?

讓我們把這三者串起來看:

  1. 單一資料來源 提供了我們觀察應用的「唯一視窗」。
  2. 狀態是唯讀的 確保了沒有人能偷偷摸摸地修改資料,所有變更都必須經過 Action 這個「正式申請」。
  3. 使用純函數進行變更 保證了狀態轉換的邏輯是透明、穩定且可逆的。

這三大原則共同創造了一個封閉且可控的循環。開發者不再需要擔心狀態像脫韁野馬一樣在專案各處亂竄。每當 UI 需要更新時,它必須大聲喊出「我想要做什麼(Action)」,然後由一個安靜且誠實的機器人(Reducer)根據規則算出新的快照(State),最後反映在單一的儀表板(Store)上。

這種設計哲學雖然在初期會增加一些 boilerplate(樣板程式碼),但它帶來的穩定性是無價的。在接下來的課程中,我們會看到 Redux Toolkit(RTK)如何透過優雅的 API 封裝,讓我們在享受這三大原則帶來的好處之餘,又不需要承受手寫繁瑣程式碼的痛苦。

重點整理

  • Predictability 是 Redux 的核心價值。
  • Single Source of Truth:全域狀態統一管理,方便除錯與 SSR。
  • State is Read-only:不可直接變動 state,必須透過 Action 表達意圖。
  • Pure Functions:Reducer 只負責計算,不准有副作用,確保結果可預測且支援時間旅行。

角色分工的序幕

理解了心法(三大原則)之後,下一步我們要進入招式(具體角色)的學習。在 Redux 的世界觀裡,有三個主要角色在維持這個循環:ActionReducerStore

如果把 Redux 比喻成一間高度自動化的工廠,Action 就是「訂單」,Reducer 就是「加工機器」,而 Store 就是「倉庫」。在下一部分中,我們將深入探討這三者的職責劃分,並透過圖解來理解資料是如何在這個系統中單向流動的。