如何有效率地管理 React 局部狀態? 這次我選擇了 Zustand!
前情提要:
目前公司前端的 code base 使用 React,並採用Redux
來管理應用程式層級的全局狀態,而各個頁面的 component 則單純使用 props 來傳遞資料。最近公司要改寫產品的其中一個頁面,並且提供新的功能。由於我們的前端 code base 一直以來都缺乏維護,其中包含一些難以閱讀且難以刪除的 legacy code,這次我們正好有機會局部重寫並重新規劃前端的資料傳遞邏輯架構。
一直以來我們的 component 都是用 props 來傳遞資料,有一些功能比較複雜的頁面元件,component tree 的深度可能達到十幾層,這使得這類型的複雜元件的 props drilling 問題非常嚴重,不僅非常難以追蹤程式碼,也有嚴重的 re-render 問題。因此,當我接到這個任務時,我研究了一下我們目前的需求以及 React 生態系中的狀態管理工具,最終我選擇使用Zustand
來管理這個頁面的局部狀態。
為何選擇使用 Zustand ?
需求: Component 層級的狀態管理
過去我習慣用 React 原生的 useContext hook
集中管理大component 的 state,來讓比較深層的 children components 直接呼叫 hook 來取得需要的 state。useContext 的好處就是非常簡單易學易用,同時又能解決 props drilling 的問題,但是 useContext 最令人可惜的點就在於,當任何一個 context 內的 state 被更新,所有使用到 useContext 的 components 都會被重新渲染,依然 沒辦法有效避免 re-render 的問題。
前面提到,目前我們使用 Redux
來管理全局狀態,我們主要拿 redux 來管理像是 User 資訊、各頁面資訊等應用程式層級的資訊。redux 隨然能夠解決上述的 props drilling 和 re-render 問題,但是把某一個頁面的某一個 component 的 "局部狀態" 放於 "全局" 來管理似乎不太合適,再加上公司的 redux 因為一些歷史因素,開發上不是很方便使用,因此我決定尋找 Redux 與 useContext 以外的解決方案。
剛好最近老闆在開會的時候不停的安利 Zustand
,花了一些時間研究後,發現 Zustand 非常易學,且對開發者也十分友善,可以用更簡短易懂的寫法 cover 掉大部分 redux 能做到的事情,且官方文件恰好有針對我目前的需求提供一個很好的解決方案,因此在這個專案中我選擇使用這個我之前從未使用過的工具。
各 state management tools 概覽
useContext
Redux
Zustand
Zustand 的優勢
- Zustand vs useContext
- 使用簡便性:Zustand 和 useContext 都相對簡單,但 Zustand 提供更多的功能和靈活性。
- 性能優化:Zustand 在管理大型和複雜狀態時表現更佳,且只會在 state 變更時才重新渲染 component
- Zustand vs Redux
- 簡潔性:Zustand 提供了更簡潔的 API,避免了 Redux 的繁瑣模板代碼。
- 設計理念:Redux 傾向於更嚴謹的架構,而 Zustand 更注重簡化和直接性。
- 學習曲線:Zustand 的學習曲線通常比 Redux 低。
- 性能:Zustand 在某些情況下可能提供更好的性能,尤其是在避免不必要的重新渲染方面。
- 擴展性和中間件:Redux 提供豐富的中間件生態系統,Zustand 則在保持輕量的同時也提供擴展性。
- 套件大小: Zustand 體積更小更輕便
Zustand 簡介
Zustand
是一款為 React 而生的狀態管理工具,以其簡潔明了的 API 和高效的性能表現,打破了傳統狀態管理工具的束縛。它不僅輕量級、不依賴於繁瑣的模板代碼,還能夠以極低的學習成本為開發者提供強大的狀態管理能力。
Zustand 的特色
- 簡單直觀的 API:Zustand 摒棄了 Redux 那種基於 reducer, action 和 middleware 的複雜設計,提供了一個更為直接和簡潔的狀態管理方式。
- 基於 Hook 的設計:Zustand 完美融合 React 的 Hook API,讓狀態管理與組件之間的聯系更加緊密,使用上更加簡易自然。
- 無需 Context Provider:Zustand 允許我們在應用的任何地方直接使用 hook 訪問狀態,無需額外包裹 Context Provider。
- 效能優化:Zustand 通過有效的狀態選擇和更新,極大地減少了不必要的組件重渲染,從而提升了整體的應用性能。
- 異步操作支持:它無縫地支持異步操作,讓處理異步狀態更新變得輕而易舉。
- 易於擴展和集成:Zustand 提供了豐富的中間件支持,如日誌記錄、數據持久化等,讓你的狀態管理更加靈活和強大。
Zustand 使用方法範例
// src/store.js
import { create } from 'zustand'
// 建立 store hook
const useBearStore = create((set) => ({
// states and actions
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}))
// src/component/Counter.js
function BearCounter() {
// use selector to get state value
const bears = useBearStore((state) => state.bears)
return <h1>{bears} around here ...</h1>
}
function Controls() {
// use selector to get action function
const increasePopulation = useBearStore((state) => state.increasePopulation)
return <button onClick={increasePopulation}>one up</button>
}
Zustand 使用小技巧
使用 component 內的資料來初始化 zustand store hook
參考 Initialize state with props (@Zustand) 後以
create
直接建立 store hook,取代教學中用createStore
建立 store 物件再配合 useStore hook 的使用方式
在 React 專案中,資料通常是透過 props 在 component 之間由上到下傳遞,在某些情況下,如果我們希望 zustand 可以幫我們集中管理特定 component 的資料狀態,這意味著以已經存在於特定 component 的資料來初始化 store ,那我們便不能以一般的使用方法,在任意一個獨立的檔案中 create store hook,再引入需要的 component 中,而是必須在 component 中接收上級 component 傳入的 props 作為初始值來 create store hook。
但問題來了,如果我們在 component 中 create store hook,那是否意味著我們只能透過 props 傳遞 store hook 給該 component 的 children 使用呢?這樣是否依然沒辦法解決 props drilling 的問題呢?
因此在這個情境下,若要解決 props drilling 的問題,我們需要用到 React 的 Context API 與 useContext 來傳遞 zustand 的 store hook。
以下提供一個實作範例
-
建立一個
createStoreHook
function,以預計會從上層 component 取得用以初始化 state 的 initProps 做為參數, 並回傳 store hook。// src/store.js
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
function createStoreHook(initProps) {
// 這邊把 initProps 解構是為了更方便閱讀,也可以不解構直接塞到 create 裡
const { state1, state2 } = initProps
const defaultProps = {
state1: -1,
state2: -1,
state3: { key1: 1, key2: 2, key3: { key3_1: 3 } },
}
return create(
// 用 immer middleware 更新 nested object
immer((set, get) => ({
...defaultProps,
state1,
state2,
setState1: () => set(() => ({ state1: get().state1 + 1 })),
setState2: () => set(() => ({ state2: get().state2 + 1 })),
setState3: () =>
set((state) => {
state.state3.key3.key3_1 += 1
}),
})),
)
} -
透過 createContext 建立 store hook 的 Context。
// src/StoreProvider.jsx
import { createContext } from 'react'
import { useRef } from 'react'
export const StoreContext = createContext(null)
export function StoreProvider({ children, ...props }) {
const storeRef = useRef()
storeRef.current = createStoreHook(props)
return <StoreContext.Provider value={storeRef.current}>{children}</StoreContext.Provider>