はじめに
状態管理ライブラリを選ぶとき、「小さく始めたいけど、後で機能が足りなくなるのは嫌だ」というジレンマに悩まされたことはないでしょうか。軽量なライブラリはシンプルですが、非同期処理やフォーム管理が必要になると途端に手が足りなくなり、逆に高機能なライブラリは最初から学習コストが重い。どちらかを妥協して選んだ経験がある方は多いはずです。
Reatomは、この悩みに「小さく始めて、必要なだけ育てる」という発想で応えるライブラリです。コアはたった1つのatomという概念だけで構成されていて、そこにcomputed(派生状態)やeffect(副作用)、さらにはwithAsyncData(非同期データ取得)やreatomForm(フォーム管理)といった拡張を後から積み重ねていけます。最初のコードが大きくなることも、途中で別のライブラリに乗り換える必要もありません。
とはいえ、読むより触った方が早いと思います。atomとcomputedだけでカウンターの偶奇を判定する最小構成を用意したので、先に挙動を見たい方はこちらからどうぞ。
Reatomとは
Reatomは「Start simple, grow easy」を掲げるJavaScript/TypeScript向けの状態管理ライブラリです。プロキシを使わない明示的なリアクティブシステムを採用しており、atomと呼ばれる最小単位の状態から、React・Vue・Solidなど複数フレームワークへのアダプタ、非同期処理、フォーム、ルーターまでを同じ設計思想の上に統一しています。
主な特徴
- 極小のコア - コアパッケージはgzip換算で2KB程度と軽量で、
atom・computed・action・effectというごく少数の概念だけで構成されています - extendによる機能拡張 -
atom(0).extend(...)のように、必要な機能だけを後付けできます。withAsyncDataで非同期取得の状態(ローディング・エラー・再試行)を、withActionsでメソッドを追加するなど、使う分だけ複雑さが増える設計です - フレームワーク非依存 - React・Vue・Solid・Preact・Litなど複数フレームワーク向けのアダプタが用意されており、状態ロジック自体はフレームワークに縛られません
- 非同期処理への配慮 -
wrap関数による非同期コンテキストの保持、withAbortによる競合状態の防止など、非同期処理特有のバグを避ける仕組みが組み込まれています
インストール
コアパッケージのみで利用できます。
npm install @reatom/core
Reactと組み合わせる場合はアダプタパッケージも追加します。
npm install @reatom/core @reatom/react
Reatomのサンプルを動かす
以下はatomとcomputedだけを使った最小構成のサンプルです。atom(0)でカウンターの状態を作り、computedでその値から「偶数かどうか」を導出し、effectで値が変わるたびにDOMへ反映しています。ボタンを押すたびにcounter.set()が呼ばれ、isEvenが自動的に再計算される様子を確認できます。
import { atom, computed, effect } from '@reatom/core'
const counter = atom(0, 'counter').extend((target) => ({
increment: () => target.set((v) => v + 1),
decrement: () => target.set((v) => v - 1),
}))
const isEven = computed(() => counter() % 2 === 0, 'isEven')
effect(() => {
console.log(counter(), isEven() ? '偶数' : '奇数')
}, 'logEffect')
実際に動かせるものが下です。ボタンを押してcounterの値を変え、isEvenの判定が即座に切り替わることを確認してみてください。
counter.set()を呼ぶだけで、それに依存するcomputedとeffectが自動的に再評価されているのが分かります。isEvenのロジックをcounter() % 3 === 0のように書き換えれば、依存関係を変えずに判定条件だけを差し替えられる点もReatomらしい部分です。
基本的な使い方
Reatomの基本は「状態をatomで作り、必要な操作をextendで足す」という流れです。
import { atom, computed, effect } from '@reatom/core'
// 状態を持つatomを作成
const page = atom(0, 'page').extend((target) => ({
next: () => target.set((v) => v + 1),
prev: () => target.set((v) => Math.max(0, v - 1)),
}))
// 派生状態はcomputedで定義(依存するatomが変わった時だけ再計算)
const isFirstPage = computed(() => page() === 0, 'isFirstPage')
// 値の変化に反応する副作用はeffectで書く
effect(() => {
console.log(`現在のページ: ${page()}`)
}, 'pageLogger')
page.next() // => 現在のページ: 1
page.next() // => 現在のページ: 2
page.prev() // => 現在のページ: 1
atomの値はそのまま関数として呼び出す(page())ことで読み取れ、.set()で更新します。computedは依存先のatomが変化したときだけ再計算される遅延評価で、effectは生成された時点から自動的に購読を開始し、依存するatomの変化に即座に反応します。
実践的なユースケース
非同期データ取得とローディング状態の管理
APIからデータを取得する処理では、ローディング中かどうか、エラーが起きたかどうかといった付随する状態の管理が煩雑になりがちです。ReatomではcomputedをwithAsyncDataで拡張することで、データ取得・ローディング・エラー・再試行までを1つのオブジェクトにまとめて扱えます。
list.data()で取得済みのデータを、list.ready()でローディング完了したかどうかを、そのまま読み取れます。再読み込みボタンを押すとlist.retry()が再取得をトリガーし、withAsyncDataが競合状態の防止まで面倒を見てくれるため、呼び出し側は状態を読むだけで済みます。
booleanやenumの状態を意味のある操作で扱う
モーダルの開閉やステータスの切り替えなど、値の種類が決まっている状態は、set(true)のような直値の代入よりも意味の伝わる操作で扱いたい場面が多くあります。ReatomはreatomBooleanとreatomEnumというヘルパーで、こうした定型的な状態に専用のアクションを自動で用意します。
import { reatomBoolean, reatomEnum } from '@reatom/core'
const isModalOpen = reatomBoolean(false, 'isModalOpen')
isModalOpen.setTrue()
isModalOpen.toggle()
const priority = reatomEnum(['low', 'medium', 'high'], 'priority')
priority.setHigh()
priority() // => 'high'
isModalOpen.toggle()やpriority.setHigh()のように、状態の意図がそのままメソッド名になっている点がreatomBoolean・reatomEnumの利点です。生のset(true)やset('high')と比べて、コードを読んだときに「何が起きるか」が一目で分かります。
Reactコンポーネントへの統合
ReactでReatomの状態を使う場合、@reatom/reactのreatomComponentでコンポーネントをラップすると、コンポーネント内でatomを関数として呼び出すだけで購読が完了します。useStateやuseSelectorのような追加のフックは不要です。
import { atom, wrap } from '@reatom/core'
import { reatomComponent } from '@reatom/react'
const page = atom(0, 'page').extend((target) => ({
next: () => target.set((v) => v + 1),
prev: () => target.set((v) => Math.max(0, v - 1)),
}))
export const Paging = reatomComponent(() => (
<span>
<button onClick={wrap(page.prev)}>prev</button>
{page()}
<button onClick={wrap(page.next)}>next</button>
</span>
), 'Paging')
reatomComponentでラップした関数の中では、page()を呼んだ箇所が自動的に購読対象になるため、pageの値が変わるとコンポーネントだけが再レンダリングされます。イベントハンドラをwrap()で包んでいるのは、非同期処理をまたいでもReatomの追跡コンテキストを保つためで、page.nextを直接そのまま渡すよりwrap(page.next)と書くことが推奨されています。
まとめ
Reatomは、atomという単一のシンプルな概念を起点に、computed・effect・extendを組み合わせることで、非同期データ取得やフォーム管理といった複雑な要件まで同じ書き方の延長線上で扱えるライブラリです。小さなカウンターから始めて、必要になった時点でwithAsyncDataやreatomComponentのような拡張を足していけるため、「最初に大きく設計しすぎる」「後から別のライブラリに乗り換える」といった状態管理ライブラリ選定にありがちな失敗を避けやすくなっています。
まずは1つのatomを作るところから、手元のプロジェクトで試してみてください。