原子化狀態(tài)管理庫(kù) Jotai,它和 Zustand 有啥區(qū)別?
共 22850字,需瀏覽 46分鐘
·
2024-04-19 01:50
Jotai 是一個(gè) react 的狀態(tài)管理庫(kù),主打原子化。
提到原子化,你可能會(huì)想到原子化 CSS 框架 tailwind。
比如這樣的 css:
<div class="aaa"></div>
.aaa {
font-size: 16px;
border: 1px solid #000;
padding: 4px;
}
用 tailwind 這樣寫:
<div class="text-base p-1 border border-black border-solid"></div>
.text-base {
font-size: 16px;
}
.p-1 {
padding: 4px;
}
.border {
border-width: 1px;
}
.border-black {
border-color: black;
}
.border-solid {
border-style: solid;
}
定義一系列原子 class,用到的時(shí)候組合這些 class。
jotai 也是這個(gè)思想:
通過(guò) atom 定義一個(gè)原子狀態(tài),可以把它組合起來(lái)成為新的狀態(tài)。
那狀態(tài)為什么要原子化呢?
來(lái)看個(gè)例子:
import { FC, PropsWithChildren, createContext, useContext, useState } from "react";
interface ContextType {
aaa: number;
bbb: number;
setAaa: (aaa: number) => void;
setBbb: (bbb: number) => void;
}
const context = createContext<ContextType>({
aaa: 0,
bbb: 0,
setAaa: () => {},
setBbb: () => {}
});
const Provider: FC<PropsWithChildren> = ({ children }) => {
const [aaa, setAaa] = useState(0);
const [bbb, setBbb] = useState(0);
return (
<context.Provider
value={{
aaa,
bbb,
setAaa,
setBbb
}}
>
{children}
</context.Provider>
);
};
const App = () => (
<Provider>
<Aaa />
<Bbb />
</Provider>
);
const Aaa = () => {
const { aaa, setAaa } = useContext(context);
console.log('Aaa render...')
return <div>
aaa: {aaa}
<button onClick={() => setAaa(aaa + 1)}>加一</button>
</div>;
};
const Bbb = () => {
const { bbb, setBbb } = useContext(context);
console.log("Bbb render...");
return <div>
bbb: {bbb}
<button onClick={() => setBbb(bbb + 1)}>加一</button>
</div>;
};
export default App;
用 createContext 創(chuàng)建了 context,其中保存了 2 個(gè)useState 的 state 和 setState 方法。
用 Provider 向其中設(shè)置值,在 Aaa、Bbb 組件里用 useContext 取出來(lái)渲染。
瀏覽器訪問(wèn)下:
可以看到,修改 aaa 的時(shí)候,會(huì)同時(shí)觸發(fā) bbb 組件的渲染,修改 bbb 的時(shí)候,也會(huì)觸發(fā) aaa 組件的渲染。
因?yàn)椴还苄薷?aaa 還是 bbb,都是修改 context 的值,會(huì)導(dǎo)致所有用到這個(gè) context 的組件重新渲染。
這就是 Context 的問(wèn)題。
解決方案也很容易想到:拆分成兩個(gè) context 不就不會(huì)互相影響了?
import { FC, PropsWithChildren, createContext, useContext, useState } from "react";
interface AaaContextType {
aaa: number;
setAaa: (aaa: number) => void;
}
const aaaContext = createContext<AaaContextType>({
aaa: 0,
setAaa: () => {}
});
interface BbbContextType {
bbb: number;
setBbb: (bbb: number) => void;
}
const bbbContext = createContext<BbbContextType>({
bbb: 0,
setBbb: () => {}
});
const AaaProvider: FC<PropsWithChildren> = ({ children }) => {
const [aaa, setAaa] = useState(0);
return (
<aaaContext.Provider
value={{
aaa,
setAaa
}}
>
{children}
</aaaContext.Provider>
);
};
const BbbProvider: FC<PropsWithChildren> = ({ children }) => {
const [bbb, setBbb] = useState(0);
return (
<bbbContext.Provider
value={{
bbb,
setBbb
}}
>
{children}
</bbbContext.Provider>
);
};
const App = () => (
<AaaProvider>
<BbbProvider>
<Aaa />
<Bbb />
</BbbProvider>
</AaaProvider>
);
const Aaa = () => {
const { aaa, setAaa } = useContext(aaaContext);
console.log('Aaa render...')
return <div>
aaa: {aaa}
<button onClick={() => setAaa(aaa + 1)}>加一</button>
</div>;
};
const Bbb = () => {
const { bbb, setBbb } = useContext(bbbContext);
console.log("Bbb render...");
return <div>
bbb: {bbb}
<button onClick={() => setBbb(bbb + 1)}>加一</button>
</div>;
};
export default App;
這樣就好了。
這種把狀態(tài)放到不同的 context 中管理,也是一種原子化的思想。
雖然說(shuō)這個(gè)與 jotai 沒(méi)啥關(guān)系,因?yàn)闋顟B(tài)管理庫(kù)不依賴于 context 實(shí)現(xiàn),自然也沒(méi)那些問(wèn)題。
但是 jotai 在介紹原子化思想時(shí)提到了這個(gè):
可能你用過(guò) redux、zustand 這些狀態(tài)管理庫(kù),jotai 和它們是完全兩種思路。
用 zustand 是這樣寫:
import { create } from 'zustand'
const useStore = create((set) => ({
aaa: 0,
bbb: 0,
setAaa: (value) => set({ aaa: value}),
setBbb: (value) => set({ bbb: value})
}))
function Aaa() {
const aaa = useStore(state => state.aaa);
const setAaa = useStore((state) => state.setAaa);
console.log('Aaa render...')
return <div>
aaa: {aaa}
<button onClick={() => setAaa(aaa + 1)}>加一</button>
</div>
}
function Bbb() {
const bbb = useStore(state => state.bbb);
const setBbb = useStore((state) => state.setBbb);
console.log('Bbb render...')
return <div>
bbb: {bbb}
<button onClick={() => setBbb(bbb + 1)}>加一</button>
</div>
}
export default function App() {
return <div>
<Aaa></Aaa>
<Bbb></Bbb>
</div>
}
store 里定義全部的 state,然后在組件里選出一部分來(lái)用。
這個(gè)叫做 selector:
狀態(tài)變了之后,zustand 會(huì)對(duì)比 selector 出的狀態(tài)的新舊值,變了才會(huì)觸發(fā)組件重新渲染。
此外,這個(gè) selector 還可以起到派生狀態(tài)的作用,對(duì)原始狀態(tài)做一些修改:
而在 jotai 里,每個(gè)狀態(tài)都是獨(dú)立的原子:
import { atom, useAtom } from 'jotai';
const aaaAtom = atom (0);
const bbbAtom = atom(0);
function Aaa() {
const [aaa, setAaa]= useAtom(aaaAtom);
console.log('Aaa render...')
return <div>
aaa: {aaa}
<button onClick={() => setAaa(aaa + 1)}>加一</button>
</div>
}
function Bbb() {
const [bbb, setBbb]= useAtom(bbbAtom);
console.log('Bbb render...')
return <div>
bbb: {bbb}
<button onClick={() => setBbb(bbb + 1)}>加一</button>
</div>
}
export default function App() {
return <div>
<Aaa></Aaa>
<Bbb></Bbb>
</div>
}
狀態(tài)可以組合,產(chǎn)生派生狀態(tài):
而在 zustand 里是通過(guò) selector 來(lái)做:
不知道大家有沒(méi)有感受到這兩種方式的區(qū)別:
zustand 是所有 state 放在全局 store 里,然后用到的時(shí)候 selector 取需要的部分。
jotai 是每個(gè) state 單獨(dú)聲明原子狀態(tài),用到的時(shí)候單獨(dú)用或者組合用。
一個(gè)自上而下,一個(gè)自下而上,算是兩種思路。
此外,異步邏輯,比如請(qǐng)求服務(wù)端接口來(lái)拿到數(shù)據(jù),這種也是一個(gè)放在全局 store,一個(gè)單獨(dú)放在原子狀態(tài)里:
在 zustand 里是這樣:
import { create } from 'zustand'
async function getListById(id) {
const data = {
1: ['a1', 'a2', 'a3'],
2: ['b1', 'b2', 'b3', 'b4']
}
return new Promise((resolve) => {
setTimeout(() => {
resolve(data[id]);
}, 2000);
});
}
const useStore = create((set) => ({
list: [],
fetchData: async (param) => {
const data = await getListById(param);
set({ list: data });
},
}))
export default function App() {
const list = useStore(state => state.list);
const fetchListData = useStore((state) => state.fetchData);
return <div>
<button onClick={() => fetchListData(1)}>列表111</button>
<ul>
{
list.map(item => {
return <li key={item}>{item}</li>
})
}
</ul>
</div>
}
在 store 里添加一個(gè) fetchData 的 async 方法,組件里取出來(lái)用就行。
可以看到,2s 后拿到了數(shù)據(jù)設(shè)置到 list,并且觸發(fā)了組件渲染。
而在 jotai 里,也是單獨(dú)放在 atom 里的:
import { atom, useAtom } from 'jotai';
async function getListById(id) {
const data = {
1: ['a1', 'a2', 'a3'],
2: ['b1', 'b2', 'b3', 'b4']
}
return new Promise((resolve) => {
setTimeout(() => {
resolve(data[id]);
}, 2000);
});
}
const listAtom = atom([]);
const fetchDataAtom = atom(null, async (get, set, param) => {
const data = await getListById(param);
set(listAtom, data);
});
export default function App() {
const [,fetchListData] = useAtom(fetchDataAtom);
const [list] = useAtom(listAtom);
return <div>
<button onClick={() => fetchListData(2)}>列表222</button>
<ul>
{
list.map(item => {
return <li key={item}>{item}</li>
})
}
</ul>
</div>
}
atom 除了可以直接傳值外,也可以分別傳入 get、set 函數(shù)。
之前的派生狀態(tài)就是只傳入了 get 函數(shù):
這樣,狀態(tài)是只讀的。
這里我們只傳入了 set 函數(shù):
所以狀態(tài)是只能寫。
用的時(shí)候要取第二個(gè)參數(shù):
當(dāng)然,這么寫有點(diǎn)費(fèi)勁,所以 atom 對(duì)于只讀只寫的狀態(tài)多了兩個(gè) hook:
useAtomValue 是讀取值,useSetAtom 是拿到寫入函數(shù)。
而常用的 useAtom 就是拿到這兩者返回值的數(shù)組。
效果一樣:
當(dāng)然,這里沒(méi)必要用兩個(gè) atom,合并成一個(gè)就行:
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
async function getListById(id) {
const data = {
1: ['a1', 'a2', 'a3'],
2: ['b1', 'b2', 'b3', 'b4']
}
return new Promise((resolve) => {
setTimeout(() => {
resolve(data[id]);
}, 2000);
});
}
const listAtom = atom([]);
const dataAtom = atom((get) => {
return get(listAtom);
}, async (get, set, param) => {
const data = await getListById(param);
set(listAtom, data);
});
export default function App() {
const [list, fetchListData] = useAtom(dataAtom);
return <div>
<button onClick={() => fetchListData(2)}>列表222</button>
<ul>
{
list.map(item => {
return <li key={item}>{item}</li>
})
}
</ul>
</div>
}
此外,用 useSetAtom 有時(shí)候可以起到性能優(yōu)化的作用。
比如這段代碼:
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
const aaaAtom = atom(0);
function Aaa() {
const [aaa] = useAtom(aaaAtom);
console.log('Aaa render...');
return <div>
{aaa}
</div>
}
function Bbb() {
const [, setAaa] = useAtom(aaaAtom);
console.log('Bbb render...');
return <div>
<button onClick={() => setAaa(Math.random())}>按鈕</button>
</div>
}
export default function App() {
return <div>
<Aaa></Aaa>
<Bbb></Bbb>
</div>
}
在 Aaa 組件里讀取狀態(tài),在 Bbb 組件里修改狀態(tài)。
可以看到,點(diǎn)擊按鈕 Aaa、Bbb 組件都重新渲染了。
而其實(shí) Bbb 組件不需要重新渲染。
這時(shí)候可以改一下:
換成 useSetAtom,也就是不需要讀取狀態(tài)值。
這樣狀態(tài)變了就不如觸發(fā)這個(gè)組件的重新渲染了:
上面 Aaa 組件里也可以簡(jiǎn)化成 useAtomValue:
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
const aaaAtom = atom(0);
function Aaa() {
const aaa = useAtomValue(aaaAtom);
console.log('Aaa render...');
return <div>
{aaa}
</div>
}
function Bbb() {
const setAaa = useSetAtom(aaaAtom);
console.log('Bbb render...');
return <div>
<button onClick={() => setAaa(Math.random())}>按鈕</button>
</div>
}
export default function App() {
return <div>
<Aaa></Aaa>
<Bbb></Bbb>
</div>
}
至此,jotai 的核心功能就講完了:
通過(guò) atom 創(chuàng)建原子狀態(tài),定義的時(shí)候還可以單獨(dú)指定 get、set 函數(shù)(或者叫 read、write 函數(shù)),用來(lái)實(shí)現(xiàn)狀態(tài)派生、異步狀態(tài)修改。
組件里可以用 useAtom 來(lái)拿到 get、set 函數(shù),也可以通過(guò) useAtomValue、useSetAtom 分別拿。
不需要讀取狀態(tài)的,用 useSetAtom 還可以避免不必要的渲染。
那 zustand 支持的中間件機(jī)制在 jotai 里怎么實(shí)現(xiàn)呢?
zustand 支持通過(guò)中間件來(lái)修改 get、set 函數(shù):
比如在 set 的時(shí)候打印日志。
或者用 persist 中間件把狀態(tài)存儲(chǔ)到 localStorage 中:
zustand 中間件的原理很簡(jiǎn)單,就是修改了 get、set 函數(shù),做一些額外的事情。
試一下:
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
const useStore = create(persist((set) => ({
count: 0,
setCount: (value) => set({ count: value})
}), {
name: 'count-key'
}))
export default function App() {
const count = useStore(state => state.count);
const setCount = useStore((state) => state.setCount);
return <div>
count: {count}
<button onClick={() => setCount(count + 1)}>加一</button>
</div>
}
jotai 里是用 utils 包的 atomWithStorage:
試一下:
import { useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
const countAtom = atomWithStorage('count-key2', 0)
export default function App() {
const [count, setCount] = useAtom(countAtom);
return <div>
count: {count}
<button onClick={() => setCount(count + 1)}>加一</button>
</div>
}
它是怎么實(shí)現(xiàn)的呢?和 zustand 的中間件有啥區(qū)別么?
看下源碼:
聲明一個(gè) atom 來(lái)存儲(chǔ)狀態(tài)值,然后又聲明了一個(gè) atom 來(lái) get、set 它。
其實(shí)和 zustand 中間件修改 get、set 方法的原理是一樣的,只不過(guò) atom 本來(lái)就支持自定義 get、set 方法。
總結(jié)
今天我們學(xué)了狀態(tài)管理庫(kù) jotai,以及它的原子化的思路。
聲明原子狀態(tài),然后組合成新的狀態(tài),和 tailwind 的思路類似。
提到原子化狀態(tài)管理,都會(huì)提到 context 的性能問(wèn)題,也就是 context 里通過(guò)對(duì)象存儲(chǔ)了多個(gè)值的時(shí)候,修改一個(gè)值,會(huì)導(dǎo)致依賴其他值的組件也跟著重新渲染。
所以要拆分 context,這也是原子化狀態(tài)管理的思想。
zustand 是所有 state 放在全局 store 里,然后用到的時(shí)候 selector 取需要的部分。
jotai 是每個(gè) state 單獨(dú)聲明原子狀態(tài),用到的時(shí)候單獨(dú)用或者組合用。
一個(gè)自上而下,一個(gè)自下而上,這是兩種思路。
jotai 通過(guò) atom 創(chuàng)建原子狀態(tài),定義的時(shí)候還可以單獨(dú)指定 get、set 函數(shù)(或者叫 read、write 函數(shù)),用來(lái)實(shí)現(xiàn)狀態(tài)派生、異步狀態(tài)修改。
組件里可以用 useAtom 來(lái)拿到 get、set 函數(shù),也可以通過(guò) useAtomValue、useSetAtom 分別拿。
不需要讀取狀態(tài)的,用 useSetAtom 還可以避免不必要的渲染。
不管是狀態(tài)、派生狀態(tài)、異步修改狀態(tài)、中間件等方面,zustand 和 jotai 都是一樣的。
區(qū)別只是一個(gè)是全局 store 里存儲(chǔ)所有 state,一個(gè)是聲明原子 state,然后組合。
這只是兩種思路,沒(méi)有好壞之分,看你業(yè)務(wù)需求,適合哪個(gè)就用那個(gè),或者你習(xí)慣哪種思路就用哪個(gè)。
