6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【React】状態管理ライブラリJotaiを使ってみる

Last updated at Posted at 2025-06-24

株式会社パレットリンクの@y-tsukahara_palettelinkです。
ReactでJotaiに触れる機会があったので学習内容を記します。
Reactには様々な状態管理ライブラリがありますが、その中の手段の一つとして参考になれば幸いです。

Jotaiとは

Jotai(ジョタイ)は、主にReactアプリケーション向けの状態管理ライブラリです。名前の由来は日本語の「状態」で各状態(state)を「atom(原子)」という単位で管理しています。

Jotaiの特徴としては以下の事が挙げられます。

  • シンプルで直感的なAPI
  • Provider不要のグローバルな状態管理
  • 依存関係のあるコンポーネントのみの再レンダリング

最小限のコアAPIだけでも非常にシンプルで強力な状態管理を可能にします。
コアAPIとしてはatom useAtom Store Providerの4つのAPIがあります。

Jotaiを使ってみよう

atomの定義

import React from 'react'
import { atom, useAtom } from 'jotai'

// 状態(atom)を定義
const countAtom = atom(0)

export default function App() {
  const [count, setCount] = useAtom(countAtom)

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
}

最初にatom関数を用いてatomを定義します。この段階ではまだ定義のみで値は保持していません。
作成には初期値を指定します。例では数値を初期値としていますが文字列や配列、オブジェクトでも大丈夫です。

// 状態(atom)を定義
const countAtom = atom(0)

useAtom

あとはuseAtomを用いてuseStateのように状態値と更新関数を返します。初期値は先ほど定義したcountAtomをとります。

const [count, setCount] = useAtom(countAtom)

Reactに触れたことがある方であれば馴染のある記述だと思います。useStateとの違いは状態のスコープがグローバルになっている為、アプリ全体で共有が可能であるという点にあります。

ComponentA.tsx
import { useAtom } from 'jotai'
import { countAtom } from './atoms'

const ComponentA = () => {
  const [count, setCount] = useAtom(countAtom)
  return <button onClick={() => setCount(count + 1)}>A: {count}</button>
}
ComponentB.tsx
import { useAtom } from 'jotai'
import { countAtom } from './atoms'

const ComponentB = () => {
  const [count] = useAtom(countAtom)
  return <div>B: {count}</div>
}

ComponentB.tsxのように読み取り専用としてuseAtomを使うこともできます。
つまり、useStateと同じような定義でuseContextと同様にグローバルに状態の共有が可能になるのです。しかも、useContextのようにProviderを必要としません。非常にシンプルでわかりやすいですよね!

Providerの活用

ちなみにjotaiでも一応Providerを使用することができます。

どんなときに使うのか?

各ツリーに異なる状態として管理して使う

const countAtom = atom(0)

function Counter() {
  const [count, setCount] = useAtom(countAtom)
  return <button onClick={() => setCount(count + 1)}>{count}</button>
}

function App() {
  return (
    <>
      <Provider>
        <Counter /> {/*  独立したStoreを持つ */}
      </Provider>
      <Provider>
        <Counter /> {/*  独立したStoreを持つ↑のとはまた別 */}
      </Provider>
    </>
  )
}

Storeについては後述にて改めて説明しますが、Jotaiは最初のuseAtom(atom)呼び出し時に内部的にStoreを生成しています。Storeは状態を管理する箱のようなイメージでStoreが作られると初期値のatomStoreの中で管理します。

Providerを使用することで最初に作られるStoreとは別の独立したStoreを作成し、管理することができます。ラップされたサブツリー間で状態が共有になるので同じatomでも状態は分離されます。

<Provider initialValues={[[textAtom, '初期テキスト']]} />

Providerサブツリー間での初期値を指定することもできます。

アンマウント時の挙動

function AppWrapper() {
  const [show, setShow] = useState(true)
  return (
    <>
      <button onClick={() => setShow(!show)}>Toggle App</button>
      {show && (
        <Provider>
          <App />
        </Provider>
      )}
    </>
  )
}

上記の場合showの初期値はtrueなのでProviderが描画されます。すなわちStoreが作られ、atomも初期化されます。showfalseになるとアンマウントされStoreが破棄されることになります。挙動としてはProviderのライフサイクルに基づいて状態のクリアが起こっているのです。

Storeの活用

Storeは状態を管理する箱のようなイメージと説明しました。Providerを使わなければ最初のuseAtom(atom)にグローバルなStoreが自動作成されます。Providerを使う場合も独立したStoreをマウント時に作成します。ProviderStoreを明示することもできます。

const modalStore1 = createStore()
const modalStore2 = createStore()

<Provider store={modalStore1}>
  <Modal />
</Provider>
<Provider store={modalStore2}>
  <Modal />
</Provider>

自動生成されるのになぜわざわざ明示する必要があるのか?

デバックやテストでの活用

// App.tsx atomの定義はatms.tsxで定義済み
import React, { useEffect } from 'react'
import { Provider, createStore, useAtom } from 'jotai'
import { countAtom } from './atoms'

const debugStore = createStore() // ← store を明示的に作成

const Counter = () => {
  const [count, setCount] = useAtom(countAtom)
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  )
}

const App = () => {
  useEffect(() => {
    // デバッグ:store 内の値を直接確認
    console.log('初期値:', debugStore.get(countAtom)) // → 0

    // 変更してみる
    debugStore.set(countAtom, 10)
    console.log('更新後:', debugStore.get(countAtom)) // → 10

    // 値の監視(subscribe)
    const unsubscribe = debugStore.sub(countAtom, () => {
      console.log('countAtom changed:', debugStore.get(countAtom))
    })

    return () => unsubscribe()
  }, [])

  return (
    <Provider store={debugStore}>
      <Counter />
    </Provider>
  )
}

export default App

明示的にStoreを作成することによって現在状態の取得や、変更、変更検知をすることができます。任意の状態をセットできる為、ユニットテストにも有効です。

store.get(atom) //現在状態の取得
store.set(atom, value) //状態をvalueに変更
store.sub(atom, callback) //変更検知

Derived atomsの活用

Jotai特有の概念でderived atomというものがあります。直訳すると派生原子となりますがどういったものなのか見ていきましょう。

import { atom } from 'jotai'

const countAtom = atom(1)
const doubledCountAtom = atom((get) => get(countAtom) * 2)

Jotaiでは定義されたatomの値を利用して新たなatomを定義することができます。
マウントされ初回のdoubledCountAtomが呼ばれるとget(countAtom)が呼ばれ、Jotai内部で依存を記録します。
countAtomが更新されると自動でdoubleAtomも再計算され、再レンダリングが走ります。

const writableDerivedAtom = atom(
  (get) => { ...読み取りロジック... },      // read function(必須)
  (get, set, arg?) => { ...書き込みロジック... } // write function(任意)
)

書き込みをすることも可能です。

const formAtom = atom({ name: '', age: 0 })

const nameAtom = atom(
  (get) => get(formAtom).name,
  (get, set, newName: string) =>
    set(formAtom, { ...get(formAtom), name: newName })
)

例のように他のアトムの状態をまとめて書き換えることで中央集権的な更新の仕組みを作ることができます。

Jotaiの基本的な使い方としてはざっとこのような感じですが、コアAPI以外にも便利な機能がたくさんあります。
直感的に状態管理ができ、理解がしやすいライブラリだなと感じました。
公式にチュートリアルもありますので気になった方は使ってみてください。

参考

パレットリンクでは、日々のつながりや学びを大切にしながら、さまざまなお役立ち記事をお届けしています。よろしければ、ぜひ「Organization」のページもご覧ください。
また、私たちと一緒に未来をつくっていく仲間も募集中です。ご興味をお持ちの方は、ぜひお気軽にお問い合わせください。一緒に新しいご縁が生まれることを楽しみにしています。

6
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?