banner
innei

innei

写代码是因为爱,写到世界充满爱!
github
telegram
twitter

使用 Jotai 抽離組件狀態的經驗

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 還提供了 useAtomValueuseSetAtomValue,可以按需使用。在沒有消費 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 類似的狀態庫,可以試試 foxactuseContextState 實現和上面大同小異。

:::

使用 useContextatom 把全局狀態收縮到組件內部#

上述使用了 useAtom 替換 useState 之後,把組件狀態外置了,導致組件不能復用了,一旦復用他的狀態都是共享的。我們可以使用 useContextatom 配合把組件的狀態再收縮到組件內部。

還是以上面的簡單例子的舉例。

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


載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。