Jotai 是一個非常優秀的基於原子模型的 React 狀態管理庫。它採用自下而上的方法來構建全局 React 狀態管理,通過組合原子來構建狀態,並根據原子依賴關係優化渲染。這解決了 React 上下文的額外重新渲染問題,並消除了對 memoization 的需求。
使用 Jotai 可以很方便的把頂層組件揉在一起的狀態(State),拆分成多次 Atom,將狀態和 UI 分離,可以很高效的管理狀態,在組件內部也可以按需獲取狀態的 Getter / Setter,減少大量的重渲染。
使用 useAtom
代替 useState
#
Jotai atom 的使用方式非常簡單,一般的只需要把 useState
的 initialValue 寫到 atom()
裡,然後直接替換 useState
即可。
const isShowAtom = atom(true)
const Foo = () => {
const [isShow, setIsShow] = useState(true)
// change to
const [isShow, setIsShow] = useAtom(isShowAtom)
}
除此之外,Jotai 還提供了 useAtomValue
和 useSetAtomValue
,可以按需使用。在沒有消費 atomValue 場景下,你大可不必使用 useAtom
,而可以直接使用 useSetAtomValue
這樣的好處是當 atomValue 變化時不會導致該組件發生重渲染。如以下的例子當父組件點擊 Button 時不會發生重渲染,只有 Bar 會發生更新:
const isShowAtom = atom(true)
const Foo = () => {
const setIsShow = useSetAtom(isShowAtom)
return <>
<Bar />
<button onClick={() => setIsShow(true)}>顯示</button>
</>
}
const Bar = () => {
const isShow = useAtomValue(isShowAtom)
// ...
}
::: info
題外話:如果不使用 Jotai,我應該如何避免 useState 導致的過多重渲染?
上面的例子中,我們可以使用 useState + useContext 來代替。
const IsShowContext = createContext(false)
const SetIsShowContext = createContext<SetStateAction<boolean>>(() => {})
const Foo = () => {
const [isShow, setIsShow] = useState(false)
return (
<IsShowContext.Provider value={isShow}>
<SetIsShowContext.Provider value={setIsShow}>
<FooImpl />
</SetIsShowContext.Provider>
</IsShowContext.Provider>
)
}
const FooImpl = () => {
const setIsShow = useContext(SetIsShowContext)
return (
<>
<Bar />
<button onClick={() => setIsShow(true)}>顯示</button>
</>
)
}
const Bar = () => {
const isShow = useContext(IsShowContext)
// ...
}
而這種為了實現性能優化而把 Context 才碎的方式,在狀態過多的情況下非常難維護。
如果你不想使用 Jotai 類似的狀態庫,可以試試 foxact 的 useContextState
實現和上面大同小異。
:::
使用 useContext
和 atom
把全局狀態收縮到組件內部#
上述使用了 useAtom
替換 useState
之後,把組件狀態外置了,導致組件不能復用了,一旦復用他的狀態都是共享的。我們可以使用 useContext
和 atom
配合把組件的狀態再收縮到組件內部。
還是以上面的簡單例子的舉例。
const FooContext = createContext(null)
const Foo = () => {
const contextValue = useRef({
isShowAtom: atom(false),
}).current
return (
<FooContext.Provider value={contextValue}>
<FooImpl />
</FooContext.Provider>
)
}
const FooImpl = () => {
const { isShowAtom } = useContext(FooContext)
const setIsShow = useSetAtom(isShowAtom)
return (
<>
<Bar />
<button onClick={() => setIsShow(true)}>顯示</button>
</>
)
}
const Bar = () => {
const { isShowAtom } = useContext(FooContext)
const isShow = useAtomValue(isShowAtom)
// ...
}
利用一個 context 非常巧妙的把狀態再次打入組件內部,從頂層傳入一個 contextValue,context 內部掛載 atom
,子組件按需消費 atom,又因為這個 Context 沒有任何依賴,所以你可以在自帶組件任意的使用 contextValue 不用擔心 contextValue 變化導致的重渲染問題,在這裡 contextValue 永遠不會變化,又得益於 Jotai,在任何地方從 contextValue 獲取 atom 然後使用 useAtomValue
訂閱 atom 的變化並響應到組件中。
按需訂閱 Atom 的利器 - selectAtom
#
Jotai 提供了 selectAtom
函數,該函數可以在原有 atom 基礎上創建一個新的 readOnly atom,主要的使用方法是實現 selector。
一般的 atomValue 如果本身就是 primitive 類型的大可不必使用這個函數。如果是引用類型的值,比如對象。根據 Immutable 的特性,龐大的對象內部需要發生更改,就會創建一個新的對象。對象內部的任何改變會導致訂閱了整個 atomValue 的組件發生更新。
const objectAtom = atom({
type: 'object',
value: {
foo: 1
}
})
const Foo = () => {
const setter = useSetAtom(objectAtom)
return <>
<Bar />
<button onClick={() => setter(prev => ({...prev, value: { foo:1 }}))}>顯示</button>
</>
}
const Bar = () => {
const { type } = useAtomValue(objectAtom)
// ...
}
在上面的例子中,雖然 Bar 只消費了 type
,但是 Foo 中的 button 點擊後,Bar 也會因為 objectAtom 的 value 改變而改變。
const objectAtom = atom({
type: 'object',
value: {
foo: 1,
},
})
const Foo = () => {
const setter = useSetAtom(objectAtom)
return (
<>
<Bar />
<button
onClick={() => setter((prev) => ({ ...prev, value: { foo: 1 } }))}
>
顯示
</button>
</>
)
}
const Bar = () => {
const type = useAtomValue(
selectAtom(
objectAtom,
useCallback((atomValue) => atomValue.type, []), // 注意這裡
),
)
// ...
}
而使用 selectAtom 提取內部的值就不會發生這樣的問題了。注意這裡需要用 useCallback
包裹傳入的 selector,或者提取這個函數到組件外部,需要保證在下次重渲染中函數不變,否則就造成重渲染地獄。
以上三點,能覆蓋到大部分場景。趕快試試吧。
此文由 Mix Space 同步更新至 xLog
原始鏈接為 https://innei.in/posts/programming/jotai-experience-with-component-state-abstraction