状態管理ライブラリのJotaiに入門する
はじめに
先日、Reactの状態管理ライブラリとして知られていたRecoilのリポジトリがアーカイブされたことが話題になりました。
This repository has been archived by the owner on Jan 2, 2025. It is now read-only.
これまでRecoilを利用していた開発者も多く、戸惑った方もいるかもしれません。その中で、RecoilからJotaiへ移行したという事例も見かけるようになりました。
私自身も今回、Jotaiを使ってみたのでその基本的な使い方をまとめてみました。
また、Jotai特有の派生状態( Derived Atom )の活用例も紹介しているので参考になったら嬉しいです。
Jotaiはどんなことができるのか
JotaiとはReact向けの状態管理ライブラリです。
主に以下のようなことが可能です。
- グローバルな状態管理
- 他の状態に依存した動的な派生状態を作成
- 非同期状態の管理
使用感はuseState
に似ていて、シンプルで学習コストが非常に低いと感じました。
この記事では2段階に分けて解説しています。
- 基本状態( Atom ≒
useState
のグローバル管理版のイメージ ) - 他の状態に依存した動的な派生状態( Derived Atom )
useState
に近い状態をグローバルに管理したい方は「基本状態 ( Atom )」を、さらに高度な状態管理を行いたい方は「派生状態 ( Derived Atom )」を参考にしてください。
基本状態 ( Atom )
atom (状態を定義)
atom と呼ばれる小さな単位で状態を管理することができます。
An atom config is an immutable object. The atom config object doesn't hold a value. The atom value exists in a store.
公式ドキュメントによると、atomは状態の定義そのものであり、実際の値は「Jotaiの内部ストア」に保存されています。
このように状態を定義します。
import { atom } from "jotai";
// 状態を定義 初期状態を「0」として定義している
const countAtom = atom(0);
useAtom (読み取り、更新関数)
useAtom
は状態の読み取りと更新関数の両方を提供します。useState
と似た感覚で使用できます。
※JSXの記述は省略しています。
import { useAtom } from "jotai";
const [count, setCount] = useAtom(countAtom); // 状態の読み取りと更新関数を取得
useAtomValue (読み取りのみ)
useAtomValue
は状態の読み取りのみを行うためのフックです。
状態を参照するだけで、更新関数は必要ない場合に使います。
import { useAtomValue } from "jotai";
const count = useAtomValue(countAtom); // 状態の読み取りのみを取得
useSetAtom (更新関数のみ)
useSetAtom
は、状態の更新のみを行うためのフック。
状態を変更するだけで、状態を参照する必要ない場合に使います。
import { useSetAtom } from "jotai";
const setCount = useSetAtom(countAtom); // 状態の更新関数のみを取得
派生状態 ( Derived Atom )
他の状態に依存した動的な状態を作成することができます。
派生状態(Derived Atom)は、他の基本状態(atom)を元に新しい値を計算したり、状態の更新ロジックを追加したりするために使用します。
派生状態は大きく3つのタイプに分けられます。
- Read-only atom
- Write-only atom
- Read-Write atom
Read-only atom
値の読み取りのみ許可し、更新は許可しません。
- 使用例:誕生日から年齢を計算し表示する
import { atom } from "jotai";
// 誕生日を管理する基本状態
const birthDateAtom = atom("2000-01-01");
// 誕生日から算出した年齢を管理する派生状態 birthDateAtomの派生(読み取り専用)
const ageAtom = atom((get) => {
const birthDate = new Date(get(birthDateAtom));
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
if (
today.getMonth() < birthDate.getMonth() ||
(today.getMonth() === birthDate.getMonth() &&
today.getDate() < birthDate.getDate())
) {
age--;
}
return age;
});
import { useAtom, useAtomValue } from "jotai";
function AgeDisplay() {
// 年齢は直接更新する必要がないため読み取り専用
const age = useAtomValue(ageAtom);
// 誕生日を更新すると自動で年齢も更新される
const [birthDate, setBirthDate] = useAtom(birthDateAtom);
return (
<div>
<p>年齢: {age}歳</p>
<label>
誕生日を変更:
<input
type="date"
value={birthDate}
onChange={(e) => setBirthDate(e.target.value)} // 誕生日を更新
/>
</label>
</div>
);
}
基本と派生2つの状態があります。
- 誕生日を管理する基本状態(
birthDateAtom
) - 誕生日から年齢を計算する派生状態(
ageAtom
)
年齢は誕生日が変更されたら決まるため、年齢の状態を直接更新する必要がありません。そのため、読み取り専用の状態として管理します。
ユーザーが誕生日を更新するとそれに基づいて年齢の状態が更新されます。
読み取り専用にすることによって年齢を計算するロジックはカプセル化されます。
Write-only atom
値の更新のみ許可し、読み取りは許可しません。
- 使用例:カウントを常に1増やす
import { atom } from "jotai";
// カウントを管理する基本状態
const countAtom = atom(0);
// 更新専用の状態: カウントを常に1増やす
const incrementAtom = atom(
null, // 読み取りは不要
(get, set) => {
set(countAtom, get(countAtom) + 1); // countAtomの値を1増やす
}
);
import { useAtom, useSetAtom } from "jotai";
function Counter() {
const [count, setCount] = useAtom(countAtom); // 現在のカウント値を読み取り、更新
const increment = useSetAtom(incrementAtom); // 更新のみ
return (
<div>
<p>現在のカウント: {count}</p>
<button onClick={increment}>カウントを増やす</button>
<button onClick={() => setCount((prev) => prev + 1)}>
カウントを増やす
</button>
</div>
);
}
基本と派生の2つの状態があります。
- カウントを管理する基本状態(countAtom)
- カウントを常に1増やす更新専用の派生状態(incrementAtom)
incrementAtom
は値を保持しません。countAtom
の更新部分をカプセル化するようなイメージで使用します。
そのため以下は同じ動作をします。
// カウントを1増やす派生状態を使用して更新する
increment()
// カウントの基本状態を直接更新する
setCount((prev) => prev + 1)
setCount
は1以外でもさまざまな数字でカウントを直接変更できてしまいます。そのため、Write-only atomはカウントを常に1増やすロジックを再利用したい場合に使用します。
Read-Write atom
値の読み取りと更新を許可します。
- 使用例:税込価格を計算しつつ、税込価格を変更すると税抜価格を逆算
// 税抜価格を管理する基本状態
const priceAtom = atom(1000);
// 税込価格を管理する派生状態(読み取り・書き込み可能)
const taxIncludedPriceAtom = atom(
(get) => get(priceAtom) * 1.1, // 読み取り: 税抜価格から税込価格を計算
(get, set, newTaxIncludedPrice) => {
set(priceAtom, newTaxIncludedPrice / 1.1); // 書き込み: 税込価格から税抜価格を逆算
}
);
function PriceCalculator() {
const [price, setPrice] = useAtom(priceAtom); // 税抜価格を管理
const [taxIncludedPrice, setTaxIncludedPrice] = useAtom(taxIncludedPriceAtom); // 税込価格を管理
return (
<div>
<p>税抜価格: {price}円</p>
<p>税込価格: {taxIncludedPrice}円</p>
<label>
税抜価格を変更:
<input
type="number"
value={price}
onChange={(e) => setPrice(Number(e.target.value))} // 税抜価格を変更
/>
</label>
<label>
税込価格を変更:
<input
type="number"
value={taxIncludedPrice}
onChange={(e) => setTaxIncludedPrice(Number(e.target.value))} // 税込価格を変更
/>
</label>
</div>
);
}
2つの状態を管理します。
- 税抜価格を管理する基本状態(priceAtom)
- 税込価格を管理する派生状態(taxIncludedPriceAtom)
ともに読み取りと更新を許可します。
ユーザーが税抜価格を更新すると税込価格が更新されます。
さらにユーザーが税込価格を直接更新すると税抜価格を逆算して更新します。
税抜価格⇄税込価格のどちらも操作でき、それぞれが同期されます。
まとめ
以上、Jotaiの使い方について解説しました。
実際に使ってみたところ、基本状態だけでも十分利用できると思いました。特にuseState
に似た感覚で扱えるところが非常に良かったです。
一方で、派生状態については、より有効な使用例を考えながら活用していきたいと思います。
また、今後は非同期状態の管理についても学習していきたいと思います。🔥
Discussion