banner
innei

innei

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

Jotaiを使用したコンポーネントの状態の抽出の経験

Jotai は、原子モデルに基づいた非常に優れた React の状態管理ライブラリです。グローバルな React の状態管理を構築するために、ボトムアップのアプローチを採用し、原子を組み合わせて状態を構築し、原子の依存関係に基づいてレンダリングを最適化します。これにより、React コンテキストの余分な再レンダリングの問題が解決され、メモ化の必要性もなくなります。

Jotai を使用すると、トップレベルのコンポーネントで結合された状態(State)を簡単に複数の Atom に分割し、状態と UI を分離して効率的に状態を管理できます。また、コンポーネント内部でも必要に応じて状態の Getter / Setter を取得することができ、大量の再レンダリングを減らすことができます。

useStateの代わりにuseAtomを使用する#

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 が変化してもこのコンポーネントは再レンダリングされません。以下の例では、親コンポーネントがボタンをクリックしても再レンダリングは発生せず、Bar のみが更新されます。

const isShowAtom = atom(true)
const Foo = () => {
  const setIsShow = useSetAtom(isShowAtom)
  
  return <>
   <Bar />
   <button onClick={() => setIsShow(true)}>Show</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)}>Show</button>
    </>
  )
}

const Bar = () => {
  const isShow = useContext(IsShowContext)
  // ...
}

ただし、このようなパフォーマンスの最適化のために Context を細分化する方法は、状態が多い場合には非常にメンテナンスが難しいです。

Jotai のような状態管理ライブラリを使用したくない場合は、foxactuseContextStateの実装を試してみることもできます。

:::

useContextatomを使用してグローバルな状態をコンポーネント内に収束させる#

上記のuseAtomuseStateに置き換えた後、コンポーネントの状態が外部に出てしまい、コンポーネントを再利用できなくなります。コンポーネントを再利用すると、その状態は共有されます。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)}>Show</button>
    </>
  )
}

const Bar = () => {
  const { isShowAtom } = useContext(FooContext)
  const isShow = useAtomValue(isShowAtom)
  // ...
}

このように、コンテキストを使用して状態を再びコンポーネント内に取り込むことができます。トップレベルでcontextValueを作成し、その中にatomをマウントします。子コンポーネントでは、必要に応じてatomを消費します。このコンテキストには依存関係がないため、コンテキストの値が変化しても再レンダリングの問題はありません。ここでは、コンテキストの値は常に変化しないため、Jotai のおかげで、どこからでもcontextValueを取得し、useAtomValueを使用してatomの変化を購読し、コンポーネントに反映することができます。

Atom を選択的に購読するためのツール - selectAtom#

Jotai は、selectAtom関数を提供しており、この関数を使用すると、既存の atom を基に読み取り専用の新しい atom を作成することができます。主な使用方法は、セレクターの実装です。

通常、atomValue がプリミティブ型である場合は、この関数を使用する必要はありません。ただし、オブジェクトなどの参照型の値の場合、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 }}))}>Show</button>
  </>
}

const Bar = () => {
  const { type } = useAtomValue(objectAtom)
  // ...
}

上記の例では、Bar はtypeのみを消費していますが、Foo のボタンをクリックすると、objectAtom の value が変わるため、Bar も変更されます。


const objectAtom = atom({
  type: 'object',
  value: {
    foo: 1,
  },
})
const Foo = () => {
  const setter = useSetAtom(objectAtom)

  return (
    <>
      <Bar />
      <button
        onClick={() => setter((prev) => ({ ...prev, value: { foo: 1 } }))}
      >
        Show
      </button>
    </>
  )
}

const Bar = () => {
  const type = useAtomValue(
    selectAtom(
      objectAtom,
      useCallback((atomValue) => atomValue.type, []),  // 注意这里
    ),
  )
  // ...
}

しかし、selectAtom を使用すると、内部の値を抽出することでこの問題を回避することができます。ただし、渡されるセレクターをuseCallbackでラップするか、この関数をコンポーネントの外部に抽出し、次の再レンダリングで関数が変わらないようにする必要があります。そうしないと、再レンダリングの問題が発生します。


上記の 3 つのポイントは、ほとんどのシナリオをカバーできます。早速試してみてください。

この記事は Mix Space からの同期更新であり、元のリンクは https://innei.in/posts/programming/jotai-experience-with-component-state-abstraction です。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。