株式会社パレットリンクの@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
との違いは状態のスコープがグローバルになっている為、アプリ全体で共有が可能であるという点にあります。
import { useAtom } from 'jotai'
import { countAtom } from './atoms'
const ComponentA = () => {
const [count, setCount] = useAtom(countAtom)
return <button onClick={() => setCount(count + 1)}>A: {count}</button>
}
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
が作られると初期値のatom
をStore
の中で管理します。
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
も初期化されます。show
がfalse
になるとアンマウントされStore
が破棄されることになります。挙動としてはProvider
のライフサイクルに基づいて状態のクリアが起こっているのです。
Storeの活用
Store
は状態を管理する箱のようなイメージと説明しました。Provider
を使わなければ最初のuseAtom(atom)
にグローバルなStore
が自動作成されます。Provider
を使う場合も独立したStore
をマウント時に作成します。Provider
にStore
を明示することもできます。
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」のページもご覧ください。
また、私たちと一緒に未来をつくっていく仲間も募集中です。ご興味をお持ちの方は、ぜひお気軽にお問い合わせください。一緒に新しいご縁が生まれることを楽しみにしています。