大家好,我是風骨,
React
技術棧發展到現在,已經出現了非常多且優秀的狀態庫,比如從早期的 Redux
和 Mobx
,到現在擁抱 Hooks 版本的 Zustand 和 Jotai。
在當前 React 18、19 的專案中,來自同一作者開源的 Zustand 和 Jotai
狀態庫我們應該如何衡量和使用呢?
本篇,筆者將和大家一起,從 設計理念
、學習成本
、原理分析
、專案適配度
幾個維度展開研究。
如果讀者對此有更好的見解和經驗,歡迎在評論區討論和指導👏🏻👏🏻。
Zustand:自上而下的中心化思想。
類似 Redux,Zustand 有 Store
集中控制的設計思想,可以按模組劃分,將一組有關的狀態聚合在一個 store
中,通過 selector
從中取出需要的部分。
Jotai:自下而上的原子化設計。
類似於 CSS Tailwind CSS 原子類的設計思想,將狀態拆分為獨立單元(原子),你可以將它們組合在一起使用。
在我們選擇一個庫到專案中時,需要考慮對團隊成員的學習成本。比如使用起來是否簡單,是否有複雜的概念和 API 用法。
不論是 Zustand 還是 Jotai 都是基於 React Hooks 函數式編程思想實現的狀態管理。
用法上只需兩步即可完成:1)全局定義 state;2)組件內消費和更新全局 state。
Zustand 使用示例
定義 store(狀態):
// src/store/useConfigStore.js
import { create } from "zustand";
const useConfigStore = create((set) => ({
theme: "light",
lang: "zh-TW",
setLang: (lang) => set({ lang }),
setTheme: (theme) => set({ theme }),
}));
export default useConfigStore;
在組件中消費和更新狀態:
// src/components/ZustandComponent.jsx
import { Fragment } from "react";
import useConfigStore from "../store/useConfigStore";
export default function ZustandComponent() {
const { theme, lang, setLang, setTheme } = useConfigStore((state) => state);
return (
<Fragment>
<div>theme: {theme}</div>
<div>lang: {lang}</div>
<button onClick={() => setLang(lang === "zh-TW" ? "en" : "zh-TW")}>
setLang
</button>
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
setTheme
</button>
</Fragment>
);
}
Jotai 使用示例
定義 atom(狀態)
// src/store/useConfigAtom.js
import { atom } from "jotai";
const themeAtom = atom("light");
const langAtom = atom("zh-TW");
export { themeAtom, langAtom };
在組件中消費和更新狀態:
// src/components/JotaiComponent.jsx
import { useAtom } from "jotai";
import { themeAtom, langAtom } from "../store/useConfigAtom";
export default function JotaiComponent() {
const [theme, setTheme] = useAtom(themeAtom);
const [lang, setLang] = useAtom(langAtom);
return (
<div>
<div>theme: {theme}</div>
<div>lang: {lang}</div>
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
setTheme
</button>
<button onClick={() => setLang(lang === "zh-TW" ? "en" : "zh-TW")}>
setLang
</button>
</div>
);
}
從上面代碼可以看出這兩個庫的用法都非常簡單,不會對開發者有太多心智壓力。
除了基礎用法外,官方還提供了一些場景用法(比如 set 的異步操作)、工具/中間件,可在下面點擊跳轉了解更多:
二者的「數據驅動組件重渲染」實現原理比較相似:都採用「觀察者模式」 subscribe
訂閱更新的方式實現。
Zustand
它採用了 React18 新增的 hook useSyncExternalStore
。
通過 useSyncExternalStore
將外部的 store(普通物件)接入到組件內,並且訂閱 store set 更新方法,產生更新後驅動組件重新渲染。
PS:如果你對 useSyncExternalStore hook 不太熟悉,可以先閱讀 React 官方文檔:點擊這裡
簡易版實現思路如下,用法與原版完全一致:
// src/store/useConfigStore.js
import { useSyncExternalStore } from "react";
function create(createState) {
// 1、創建 store
const store = {
state: undefined, // store state
listeners: new Set(), // 訂閱集合
// 訂閱函數
subscribe: (listener) => {
store.listeners.add(listener);
return () => {
store.listeners.delete(listener);
};
},
};
// 自訂義更新 state 的方法,用於實現 listeners 通知更新
const setState = (partial, replace) => {
const { state, listeners } = store;
const nextState = typeof partial === "function" ? partial(state) : partial;
// 使用 Object.is 比較兩個物件是否是同一個引用地址,等同於使用全等 ===
if (!Object.is(nextState, state)) {
const previousState = state;
// 更新 state,如果 replace 為 true,則直接替換,否則淺合併到 state 上
store.state = replace ? nextState : Object.assign({}, state, nextState);
// notify 通知
listeners.forEach((listener) => listener(store.state, previousState));
}
};
store.state = createState(setState);
// 2、返回的 useStore 用於在組件中消費 store state
return function useStore(selector) {
return useSyncExternalStore(store.subscribe, () => selector(store.state));
};
}
在這裡,核心是自訂義了 setState
方法,在更新 store state
後,發布通知從而觸發 useSyncExternalStore
的重渲染機制。
Jotai
Jotai 和 Zustand 的實現基本相似,都是自訂義了 setState
方法,數據更新後通知訂閱回調。
不過在組件處理更新上稍有差異:Jotai 並未使用 useSyncExternalStore
hook,而是採用 useReducer
hook 來觸發組件更新。
不管用哪種方式,它們的目的都是為了在數據更新後觸發組件重新渲染。
簡易版實現思路如下,用法與原版完全一致:
// src/store/useConfigAtom.js
import { useReducer, useEffect } from "react";
// 1、創建 atom config 對象
function atom(initialState) {
const config = {
state: initialState, // 原子值
listeners: new Set(), // 訂閱集合
// 訂閱函數
subscribe: (listener) => {
config.listeners.add(listener);
return () => {
config.listeners.delete(listener);
};
},
// 自訂義更新 state 的方法,用於實現 listeners 通知更新
setState: (partial) => {
const { state, listeners } = config;
const nextState =
typeof partial === "function" ? partial(state) : partial;
if (!Object.is(nextState, state)) {
const previousState = state;
// 更新 state
config.state = nextState;
// notify 通知
listeners.forEach((listener) => listener(config.state, previousState));
}
},
};
return config;
}
// 2、實現 useAtom,用於在組件中消費和更新 atom config 對象
export function useAtom(atom) {
return [useAtomValue(atom), useSetAtom(atom)];
}
// get,簡單理解:
// 1)從 atom config 對象中獲取 state value 值
// 2)訂閱更新
// 3)收到更新後執行 useReducer dispatch 更新組件
export function useAtomValue(atom) {
const [[value], dispatch] = useReducer(
(prev) => {
const nextValue = atom.state; // 從 store 獲取最新的 atom 值
// 如果都沒有變化,返回之前的狀態(避免不必要的重新渲染)
if (Object.is(prev[0], nextValue)) return prev;
return [nextValue]; // 有變化時返回新狀態
},
undefined,
() => [atom.state] // 初始化函數
);
// useReducer 更新策略與訂閱機制配合使用
useEffect(() => {
const unsubscribe = atom.subscribe(dispatch); // 訂閱更新,執行 useReducer dispatch 更新組件
return unsubscribe;
}, [atom]);
return value;
}
// set,簡單理解:
// 1)更新 atom config 對象的 state value 值;
// 2)觸發 listeners 監聽回調完成消費此 atom state 的組件更新;
export function useSetAtom(atom) {
return atom.setState;
}
現在我們從二者的原理實現來看,它們最主要的區別還是設計思想
:是採用 store 中心化集中管理思想 還是說 atom 原子化獨立的思想。
最後我們討論一下 Zustand 和 Jotai 分別適合在什麼樣的專案中使用?
從原理上來說,它們在 React 框架上的接入基本一致;
從用法簡易度來說,它們其實都很簡單(Jotai 原子化思想,可能比 Zustand 在用法上還要更簡單一些);
筆者認為應該考量的是設計思想。
比如團隊成員習慣了 Redux 編程思維傾向這種 Store 集中管理思想,或做一些大型專案,可以選擇 Zustand
作為狀態庫使用;
反之編程思維傾向原子化獨立、組合的思想,或做一些小型專案,那可以選擇 Jotai
,更靈活一些。
具體選擇可因團隊、專案業務方向而定。
感謝閱讀,如有指點之處,歡迎"各路大俠們"提出!👏🏻👏🏻