React初心者でもわかるJotai活用法

Reactで開発を行っていると状態を維持するためのStoreという機能をほぼ必ず使うと思います。StoreはReactで提供されている基本的な機能であり便利なのですが、少々学習コストが高めだったり実装コストがそれなりにあったりしますよね。

Store以外にも状態管理をするためのパッケージはいくつかありますが、今回はJotaiというパッケージを使った状態管理と特徴について紹介します。

Jotaiとは?

Reactベースの状態管理パッケージで、Meta社が開発していたRecoilのメンテナンスが終了し構想の似ているJotaiが選ばれるようになってきました。Jotaiのメリットとして、以下の点が挙げられます。

  • useStateよりシンプルに利用できる
  • 複数のコンポーネントで状態を再利用できる

デメリットとしては、以下の点が挙げられます。

  • 状態保持のスコープがuseStateと比べて広くなりがち

Jotaiの基本構成

Jotaiの基本構成は、シンプルに以下の4つの要素で構成されています。それぞれ、どういった役割を持っているのか簡単に解説していきます。

  • atom
  • useAtom
  • Store
  • Provider

atom

保持する値の定義した情報を元に atom config を作成し、実際の値はatomでは保持せずStoreに格納されます。作ったatomはイミュータブルなオブジェクトとして生成されます。 atomでは、以下の4つのパターンで初期値を与えることができます。
  • InitialValue
  • Read-only atom
  • Write-only atom
  • Read-Write atom

InitalValueの場合は、以下のようにReactのuseStateと同様の初期化になります。この時、値はそのatomに紐づいた形でStoreに保存されます。

const productAtom = atom({ id: 12, name: 'wallet', price: 100 })

Read-only atom / Write-only atom / Read-Write atom

関数や式を初期値として与えることができ、他のatomの値を元に取得・保存などができます。 これらの式は、getやsetをするときに評価・実行されます。

const readOnlyAtom = atom((get) => get(productAtom).price * 2)

const writeOnlyAtom = atom(
  (get, set, update) => {
    set(productAtom, { ...get(priceAtom),  price: update.discount })
  },
)

const readWriteAtom = atom(
  (get) => get(productAtom).price * 2,
  (get, set, newPrice) => {
    set(productAtom, { ...get(priceAtom),  price: update.discount }
  },
)

useAtom

値を取得・保存するためのインターフェースで、上記のatomを使ってStoreから値を取得したり、Storeへ値を保存したりすることができます。Jotaiのhookには、他にも取得専用のuseAtomValueや保存専用のuseSetAtomといたhookがあります。

const Component = () => {
  const [product, setProduct] = useAtom(productAtom)
  useEffect(() => {
    setProduct({ id: 12, name: 'wallet', price: 120 })
  }, [])
}

Store

atomで定義された値を保持するための領域で、値を取得したり変更を検知したりすることができます。このStoreには、Providerを通してhookからアクセスすることができます。また、getDefaultStore()を呼び出すことで、デフォルトのStoreを参照することもできます。

const myStore = createStore()
// const defaultStore = getDefaultStore()

const countAtom = atom(0)
myStore.set(countAtom, 1)
const unsub = myStore.sub(countAtom, () => {
  console.log('countAtom value is changed to', myStore.get(countAtom))
})
// unsub() to unsubscribe

const Root = () => (
  <Provider store={myStore}>
    <App />
  </Provider>
)

Provider

Storeの値にProvider以下のコンポーネントからのアクセスからのみスコープを限定することができます。Providerを使わない場合、デフォルトでグローバルスコープのStoreが利用されますが、ページやコンポーネントごとに状態を分けたり、データが膨大にならないようにしたい場合などに利用できます。

デフォルトのグローバルスコープを利用すると多くの箇所からアクセスができ依存が増えるため、複雑性が増え柔軟性が減っていきバグや想定外の挙動が発生しやすくなることがあります。また、依存関係が複雑になっていくと修正も難しくなっていくので注意が必要です。

const Root = () => (
  <Provider store={myStore}>
    <App />
  </Provider>
)