React開発者必見!Jotaiで変わる状態管理

はい、承知いたしました。React開発者向けに、Jotaiによる状態管理の変化について詳細に説明する約5000語の記事を作成します。


React開発者必見!Jotaiで変わる状態管理

はじめに:Reactの状態管理、その進化と課題

React開発に携わる皆さんなら、アプリケーションの規模が大きくなるにつれて、状態管理が複雑になるという共通の課題に直面したことがあるでしょう。コンポーネントローカルな状態(useState)では追いつかなくなり、コンポーネントツリーの奥深くにある状態を共有するためにProps Drillingが発生したり、Context APIを使ってもパフォーマンスや管理の課題が出てきたりします。

多くの開発者は、これらの課題を解決するために、Reduxのような著名な状態管理ライブラリを導入してきました。Reduxは一元化されたストアと厳格なルールによって予測可能な状態管理を可能にしましたが、その設定にはボイラープレートが多く、学習コストも低いとは言えませんでした。その後、Context APIの進化やHooksの登場により、よりシンプルな状態管理の手法も登場しましたが、依然として大規模なアプリケーションにおける最適な状態管理手法は模索されています。

そんな中、近年注目を集めているのが「Jotai(ジョタイ)」です。Jotaiは、Recoilの設計思想にインスパイアされつつ、よりシンプルで柔軟、そしてTypeScriptとの相性が非常に良い状態管理ライブラリとして登場しました。その哲学は「プリミティブベース」。つまり、状態を小さな単位(Atom)として定義し、それらを組み合わせてより複雑な状態やロジックを構築していくというアプローチです。

本記事では、なぜ今JotaiがReactの状態管理を変革しうるのか、その核心に迫ります。Jotaiの基本的な概念から、実践的な使い方、高度なパターン、既存ライブラリとの比較、そして大規模アプリケーションでの活用法まで、詳細かつ網羅的に解説していきます。React開発者として、Jotaiを知ることは、これからの状態管理のあり方を考える上で間違いなく大きな一歩となるでしょう。

パート1:Reactにおける伝統的な状態管理手法と課題

Jotaiの素晴らしさを理解するためには、まずReactにおける従来の状態管理手法がどのようなものであったか、そしてそれぞれが抱える課題を明確にしておく必要があります。

1. コンポーネントローカルな状態 (useState, useReducer)

最も基本的で、そして最も一般的に使われるのが、コンポーネント自身の内部で状態を管理する手法です。

“`javascript
import React, { useState } from ‘react’;

function Counter() {
const [count, setCount] = useState(0);

return (

Count: {count}

);
}
“`

利点:

  • 非常にシンプルで理解しやすい。
  • コンポーネントのスコープ内に閉じているため、他の部分への影響がない。
  • React標準の機能であり、追加のライブラリは不要。

課題:

  • 状態の共有: 異なるコンポーネント間で同じ状態を共有したり、親から子へ深く状態を渡したりする場合に問題が生じます。特に、離れたコンポーネント間で状態を共有するには、共通の祖先コンポーネントまで状態を引き上げ(State Lifting)、Propsとして子孫に渡していく必要があります(Props Drilling)。
  • Props Drilling: コンポーネントツリーを何階層も経由してPropsを渡していく必要があり、コードの見通しが悪化し、リファクタリングが困難になります。途中のコンポーネントはそのPropsを使わないにも関わらず、単に受け渡しの役割を果たすだけになり、関心の分離が損なわれます。
  • 複雑な状態ロジック: useStateでは、関連する複数の状態を管理したり、状態遷移に複雑なロジックが必要な場合にコードが煩雑になりがちです。useReducerはこれを改善しますが、それでもグローバルな状態管理には向きません。

2. Context API

Props Drillingの問題を解決するために、Reactに標準で備わっているのがContext APIです。Context APIを使えば、コンポーネントツリーを下方向にデータを「注入」し、途中のコンポーネントを経由することなく、指定したContextを消費するコンポーネントでデータを受け取ることができます。

“`javascript
import React, { createContext, useContext, useState } from ‘react’;

const CountContext = createContext();

function CountProvider({ children }) {
const [count, setCount] = useState(0);
return (

{children}

);
}

function CounterDisplay() {
const { count } = useContext(CountContext);
return

Count: {count}

;
}

function CounterButtons() {
const { setCount } = useContext(CountContext);
return (


);
}

// アプリケーションの一部
function App() {
return (




);
}
“`

利点:

  • Props Drillingを回避できる。
  • React標準の機能であり、追加ライブラリは不要。
  • 比較的シンプルに、コンポーネントツリー内の広範囲で状態を共有できる。

課題:

  • パフォーマンス問題: Contextの値が更新されると、そのContextを消費している全てのコンポーネントが再レンダリングされる可能性があります。値がオブジェクトの場合、オブジェクトの参照が変わるたびにProvider配下のコンポーネントが多く再レンダリングされ、パフォーマンスが悪化しやすいです。useMemouseCallbackでProviderに渡す値をメモ化するなどの対策が必要ですが、それでも限界があります。
  • Context Hell: アプリケーションの規模が大きくなり、共有したい状態の種類が増えると、Contextが乱立し、Providerのネストが深くなる「Context Hell」状態になりがちです。
  • 状態の分割と関連: Contextは通常、一つのProviderが一つの関連する状態のまとまりを管理します。状態の一部だけが必要な場合でも、Context全体を消費する必要があり、不要な依存や再レンダリングを引き起こす可能性があります。

3. Redux

長らくReactの状態管理のデファクトスタンダードであったのがReduxです。Fluxアーキテクチャに基づき、状態を一元化されたストアで管理し、ActionとReducerを通じてのみ状態を更新するという厳格なルールを定めています。

“`javascript
// actions.js
const increment = () => ({ type: ‘INCREMENT’ });
const decrement = () => ({ type: ‘DECREMENT’ });

// reducer.js
const initialState = { count: 0 };
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case ‘INCREMENT’:
return { …state, count: state.count + 1 };
case ‘DECREMENT’:
return { …state, count: state.count – 1 };
default:
return state;
}
};

// store.js
import { createStore } from ‘redux’;
const store = createStore(counterReducer);

// component.js (with react-redux)
import React from ‘react’;
import { useSelector, useDispatch } from ‘react-redux’;
import { increment, decrement } from ‘./actions’;

function Counter() {
const count = useSelector(state => state.count);
const dispatch = useDispatch();

return (

Count: {count}


);
}
“`

利点:

  • 予測可能性: 状態の更新がActionとReducerを通じてのみ行われるため、状態遷移が非常に予測しやすいです。デバッグが容易になります。
  • 一元管理: アプリケーション全体の状態が一つのストアに集約されるため、全体像を把握しやすいです。
  • 強力なエコシステム: Redux DevTools、Middleware (Thunk, Sagaなど) による非同期処理の管理、さまざまなUtilityライブラリなど、非常に成熟したエコシステムがあります。

課題:

  • ボイラープレート: Action Typeの定義、Action Creator、Reducer、Storeの設定など、状態管理の単位ごとに書くべきコードが多くなりがちです。特に、TypeScriptを使う場合はさらに型定義の記述が増えます。
  • 学習コスト: Fluxアーキテクチャの概念(Store, Action, Dispatcher, Reducer)を理解する必要があります。非同期処理のためのMiddleware(Redux ThunkやRedux Saga)はさらに学習コストが高いです。
  • 柔軟性の欠如: 厳格なルールが予測可能性をもたらす一方で、ちょっとした状態を扱うだけでもReduxの仕組みに乗せる必要があり、柔軟性に欠けると感じる場面があります。
  • コードの分離: Action、Reducer、Selectorなどが別々のファイルに分散しやすく、関連するロジックを追うのが難しい場合があります(ただし、Redux Toolkitの登場によりこの点は大きく改善されました)。

4. Recoil

Facebook(現Meta)によって開発されたRecoilは、これらの課題、特にContext APIのパフォーマンス問題とReduxのボイラープレート問題に対するReactフレンドリーな解決策として登場しました。AtomsとSelectorsという概念を導入し、ReactのSuspenseやConcurrent Modeとの親和性を高く設計されています。

“`javascript
import React from ‘react’;
import {
atom,
selector,
useRecoilState,
useRecoilValue,
useSetRecoilState,
} from ‘recoil’;

// Atom: 状態の最小単位
const countState = atom({
key: ‘countState’, // グローバルにユニークなキー
default: 0,
});

// Selector: Atomや他のSelectorから派生した状態
const doubleCountState = selector({
key: ‘doubleCountState’,
get: ({ get }) => {
const count = get(countState);
return count * 2;
},
});

function Counter() {
const [count, setCount] = useRecoilState(countState); // 読み書き
const doubleCount = useRecoilValue(doubleCountState); // 読み取り専用

return (

Count: {count}

Double Count: {doubleCount}

);
}

// Providerが必要 (RecoilRoot)
// ReactDOM.render(, …)
“`

利点:

  • AtomとSelectorによるシンプルでReact的なAPI。
  • Hooksとの親和性が高い。
  • 状態のきめ細かい更新が可能で、Context APIよりもパフォーマンスに優れることが多い。
  • 派生状態(Selector)の定義が直感的。
  • SuspenseやConcurrent Modeをサポート。

課題:

  • キーの管理: AtomやSelectorにはグローバルにユニークなキーが必要で、このキーの衝突を防ぐ管理が必要になります。
  • Providerの必須性: Context APIと同様に、状態を利用するコンポーネントツリーのルートにProvider(RecoilRoot)が必要です。
  • 成熟度: Reduxのエコシステムに比べると歴史が浅く、ライブラリやツールが少ない場合があります。
  • ドキュメント/コミュニティ: Reduxほど日本語の情報が多くない場合があります。

パート2:Jotaiの登場 – プリミティブベースの状態管理

RecoilのAtom/Selectorモデルは多くの開発者に受け入れられましたが、そのキー管理の煩雑さや、特定のユースケースでのTypeScriptの扱いづらさなどが指摘されることもありました。ここで登場するのがJotaiです。

Jotaiは、Recoilの「状態を小さな単位(Atom)として扱う」という考え方をさらに推し進め、「Atomこそが状態のプリミティブである」という哲学のもとに設計されています。JotaiのAPIは驚くほどシンプルで、状態の定義から利用までを非常に直感的に行うことができます。

Jotaiの核心:atom

Jotaiの全てはatomから始まります。atomは、状態の最小単位であり、一つ一つの独立した状態を表現します。これはJavaScriptのプリミティブ値(文字列、数値、真偽値)やオブジェクト、配列など、どんな値でも保持できます。

atomは関数のように見えますが、これは状態の「定義」であり、それ自体が現在の値を保持しているわけではありません。Atomが保持する実際の値は、後述するProvider内で管理されます。

最も基本的なatomの定義は、初期値を渡すだけです。

“`javascript
import { atom } from ‘jotai’;

// 数値を保持するatom
const countAtom = atom(0);

// 文字列を保持するatom
const textAtom = atom(‘hello’);

// オブジェクトを保持するatom
const userAtom = atom({ name: ‘Alice’, age: 30 });

// 配列を保持するatom
const itemsAtom = atom([‘apple’, ‘banana’]);
“`

これらのatomは、Reactコンポーネントの外で定義されることが一般的です。これにより、状態の定義がコンポーネントのロジックから分離され、再利用しやすくなります。

Atomの読み書き:useAtomフック

定義したatomをコンポーネント内で利用するには、useAtomフックを使います。useAtomは、ReactのuseStateフックと同様に、現在のAtomの値と、その値を更新するためのセッター関数のペアを返します。

“`javascript
import React from ‘react’;
import { atom, useAtom } from ‘jotai’;

// カウンターのatom定義
const countAtom = atom(0);

function Counter() {
// countAtom の値を読み込み、更新関数を取得
const [count, setCount] = useAtom(countAtom);

return (

Count: {count}

{/ セッター関数を使って値を更新 /}

);
}
“`

このコードを見てください。どこかでContextを定義したり、Reducerを書いたり、Actionを定義したりする必要はありません。atomを定義して、useAtomで使うだけです。驚くほどシンプルです。

useAtomuseStateとよく似ていますが、決定的に異なるのは、useAtomが返す状態はグローバルに共有される(またはProviderスコープで共有される)という点です。複数のコンポーネントで同じcountAtomに対してuseAtomを使えば、それらはすべて同じcountの値を参照し、どれか一つがsetCountを呼び出せば、他のコンポーネントも新しい値で再レンダリングされます。

Atomの読み取り専用 (useAtomValue)

Atomの値は読みたいだけ、更新はしない、というコンポーネントもあります。そのような場合は、useAtomValueフックを使うことで、値だけを取得できます。これはパフォーマンス最適化に役立ちます。値が更新されても、その値を利用しているコンポーネントだけが再レンダリングされます。

“`javascript
import React from ‘react’;
import { atom, useAtomValue } from ‘jotai’;

const messageAtom = atom(‘Hello, Jotai!’);

function MessageDisplay() {
// messageAtom の値を読み取り専用で取得
const message = useAtomValue(messageAtom);
return

{message}

;
}
“`

Atomの書き込み専用 (useSetAtom)

逆に、Atomの値を更新したいだけで、現在の値は必要ないという場合もあります。このような場合は、useSetAtomフックを使うことで、セッター関数だけを取得できます。これもまた、不要な値の変更による再レンダリングを防ぐパフォーマンス最適化になります。

“`javascript
import React from ‘react’;
import { atom, useSetAtom } from ‘jotai’;

const notificationAtom = atom(”);

function NotificationInput() {
// notificationAtom のセッター関数だけを取得
const setNotification = useSetAtom(notificationAtom);
const [inputValue, setInputValue] = React.useState(”);

const handleSubmit = () => {
setNotification(inputValue);
setInputValue(”);
};

return (

setInputValue(e.target.value)} />

);
}
“`

このように、useAtomuseAtomValueuseSetAtomを使い分けることで、コンポーネントが必要なものだけをAtomから取得し、不必要な再レンダリングを最小限に抑えることができます。

Providerについて

JotaiはデフォルトではReactのContext APIを利用して状態を管理します。そのため、状態を共有するためには、コンポーネントツリーのルートにProviderを配置するのが最も簡単な方法です。

“`javascript
import { Provider } from ‘jotai’;
import App from ‘./App’; // jotaiを使うコンポーネントが含まれる

ReactDOM.render(
{/ ここにProviderを配置 /}

,
document.getElementById(‘root’)
);
“`

ただし、Jotaiはデフォルトでグローバルなストアを持つため、アプリケーション全体で単一のストアで十分な場合は、明示的に<Provider>を配置しなくても動作します。しかし、これはテストの際にストアをモックしたり、複数の独立したストアが必要な場合(例: Micro Frontend)に不便になることがあります。明示的に<Provider>をルートに配置することが推奨されています。

Providerはネストすることも可能です。これにより、特定のサブツリー内だけで有効なAtomを定義したり、テスト時に特定のProviderでAtomの値をオーバーライドしたりといった高度な使い方ができます。

なぜJotaiは状態管理を変えるのか? – プリミティブとボトムアップ

従来の多くの状態管理ライブラリは、アプリケーション全体の状態を一つの大きなオブジェクト(ストア)として管理し、そのストアに対する更新をReducerのような仕組みで制御するという、トップダウンまたは集中型のアプローチをとっていました。

Jotaiのアプローチは全く異なります。Atomはそれぞれが独立した小さな状態の「プリミティブ」です。アプリケーションの状態は、これらの無数の小さなAtomの集まりとして考えられます。必要なときに必要なAtomを定義し、それらをコンポーネントから直接利用する。これはまさにボトムアップのアプローチです。

このプリミティブベース・ボトムアップのアプローチが、Jotaiを特徴づけ、いくつかの重要な利点をもたらします。

  1. 驚くほどのシンプルさ: 複雑な概念や儀式(Reducer、Action、Middlewareなど)を学ぶ必要がありません。状態はAtomとして定義し、フックで利用する、ただそれだけです。学習コストが非常に低いです。
  2. 最小限のAPI: atomuseAtomuseAtomValueuseSetAtomあたりを覚えれば基本的なことはほとんどできてしまいます。APIが少ないため、迷うことがありません。
  3. 高い柔軟性: グローバルな状態、コンポーネントツリーの一部で共有される状態、派生状態、非同期状態など、様々な種類の状態を同じatomという概念で扱うことができます。
  4. 優れた開発者体験: ボイラープレートが少なく、関連するコード(状態の定義と利用)がコンポーネントの近くに配置されることが多いため、コードの見通しが良くなります。特にTypeScriptとの相性が抜群で、型推論が強力に働きます。

次のパートでは、この「プリミティブベース」という考え方が、どのようにしてより複雑な状態管理のパターンに対応していくのかを見ていきます。

パート3:Jotaiの応用 – 派生アトム、非同期処理、アトムファミリー

基本的なAtomの読み書きだけでなく、Jotaiはより複雑な状態やロジックを扱うための強力なパターンを提供しています。これらもまた、Atomを「プリミティブ」として組み合わせるという哲学に基づいています。

派生アトム (Derived Atoms)

あるAtomの値に基づいて計算される状態や、他のAtomの値に依存する状態は、新しいAtomとして定義できます。これを「派生アトム」と呼びます。派生アトムは、値を計算するための関数を渡して定義します。この関数は、他のAtomの値を取得するためのget関数を引数として受け取ります。

“`javascript
import { atom } from ‘jotai’;

const countAtom = atom(0); // 基本となる数値アトム

// countAtom の値の2倍を計算する派生アトム(読み取り専用)
const doubleCountAtom = atom((get) => get(countAtom) * 2);

// countAtom の値が偶数かどうかを判定する派生アトム(読み取り専用)
const isEvenAtom = atom((get) => get(countAtom) % 2 === 0);

// countAtom の値に基づいてメッセージを生成する派生アトム(読み取り専用)
const messageAtom = atom((get) => {
const count = get(countAtom);
return count > 10 ? ‘Count is large!’ : ‘Count is small.’;
});
“`

派生アトムは、自身が依存しているAtom(上記の例ではcountAtom)の値が更新されると自動的に再計算されます。そして、その派生アトムの値を利用しているコンポーネントだけが再レンダリングされます。これはReduxのセレクターに似ていますが、よりシンプルに、Atomを組み合わせて定義できます。

読み書き可能な派生アトム

派生アトムは読み取り専用だけでなく、読み書き可能にすることもできます。これは、Atomの定義に関数ペア(getterとsetter)を渡すことで実現します。

“`javascript
import { atom } from ‘jotai’;

const firstNameAtom = atom(‘John’);
const lastNameAtom = atom(‘Doe’);

// 読み書き可能な派生アトム:フルネーム
const fullNameAtom = atom(
// Getter: firstNameAtomとlastNameAtomを組み合わせてフルネームを生成
(get) => ${get(firstNameAtom)} ${get(lastNameAtom)},
// Setter: フルネームを受け取り、firstNameAtomとlastNameAtomを更新
(get, set, fullName) => {
const [firstName, …lastNameParts] = fullName.split(‘ ‘);
const lastName = lastNameParts.join(‘ ‘);
set(firstNameAtom, firstName);
set(lastNameAtom, lastName);
}
);

function NameEditor() {
const [fullName, setFullName] = useAtom(fullNameAtom); // fullNameAtom を利用

return (

setFullName(e.target.value)} // fullNameAtom を更新
/>

First Name: {useAtomValue(firstNameAtom)}

{/ 参照 /}

Last Name: {useAtomValue(lastNameAtom)}

{/ 参照 /}

);
}
“`

この例では、fullNameAtomという派生アトムを定義しています。このAtomを更新(setFullNameを呼び出し)すると、その内部のsetter関数が実行され、依存しているfirstNameAtomlastNameAtomが更新されます。これにより、UI上はフルネームを編集しているように見えつつ、内部の状態は名と姓に分割して管理することができます。これは複雑なフォームの状態管理などで非常に強力なパターンです。

非同期アトム (Async Atoms)

Jotaiでは、非同期処理の結果を保持するAtomを簡単に定義できます。Atomの初期値またはgetter関数としてPromiseを返す関数を渡すだけです。

“`javascript
import { atom, useAtom } from ‘jotai’;
import { Suspense } from ‘react’;

// ユーザーデータをフェッチする非同期アトム
const userAtom = atom(async () => {
console.log(‘Fetching user…’);
const response = await fetch(‘https://jsonplaceholder.typicode.com/users/1’);
const data = await response.json();
console.log(‘User fetched:’, data);
return data; // Promiseを解決した値がAtomの値となる
});

function UserDisplay() {
// useAtom は非同期アトムの場合、Promiseを解決するまでSuspenseがfallbackを表示する
const [user] = useAtom(userAtom);

return (

User Info

Name: {user.name}

Email: {user.email}

);
}

function App() {
return (
Loading user…\

}> {/ Suspenseで囲む /}


);
}
“`

非同期アトムを使う場合、そのAtomを読み込むコンポーネントは、ReactのSuspenseコンポーネントで囲む必要があります。Suspenseは、非同期処理が完了するまでの間、fallbackプロパティで指定した内容を表示します。

非同期アトムの読み込み・エラー状態のハンドリング:

useAtomフックは、非同期アトムに対して使うと、値だけでなく、そのPromiseの状態(保留中、解決済み、拒否済み)も提供します。これにより、Suspenseを使わずに、手動でローディング状態やエラー状態を扱うことも可能です。

“`javascript
import React from ‘react’;
import { atom, useAtom } from ‘jotai’;

const asyncDataAtom = atom(async () => {
// サンプル用の遅延
await new Promise(resolve => setTimeout(resolve, 1000));
if (Math.random() > 0.7) throw new Error(‘Failed to load!’);
return ‘Async data loaded!’;
});

function AsyncDataDisplay() {
const [data, setData, { loading, error }] = useAtom(asyncDataAtom);

if (loading) return

Loading…

;
if (error) return

Error: {error.message}

;

return (

{data}

{/ セッターに関数を渡すと再実行 /}

);
}
“`

useAtomが返す配列の第3要素は、非同期の状態に関する情報を持つオブジェクトです。loadingtrueの間はPromiseが保留中、errorにErrorオブジェクトが入っている場合は拒否済みです。解決済みの値は第1要素のdataに入ります。また、非同期アトムのセッター関数を引数なしで呼び出すと、getter関数が再実行され、データを再フェッチできます。

アトムファミリー (Atom Families)

リスト表示されるアイテムそれぞれが固有の状態を持つ場合(例: ToDoリストの個々のToDoの完了状態、商品の数量など)、そのアイテムのIDごとにAtomを動的に生成したいことがあります。このような場合に「アトムファミリー」が役立ちます。JotaiではatomFamilyユーティリティを使って定義します。

atomFamilyはファクトリ関数を返します。このファクトリ関数にキー(通常はアイテムのID)を渡すと、そのキーに対応するAtomが生成されます。

“`javascript
import { atom, useAtom } from ‘jotai’;
import { atomFamily } from ‘jotai/utils’; // jotai/utils からインポート

type Todo = {
id: number;
text: string;
isCompleted: boolean;
};

// Todoアイテムのリスト全体を保持するアトム
const todosAtom = atom([
{ id: 1, text: ‘Learn Jotai’, isCompleted: false },
{ id: 2, text: ‘Build App’, isCompleted: false },
]);

// TodoのIDをキーとするアトムファミリー
// 各Todoの完了状態だけを管理する
const todoCompletedAtomFamily = atomFamily((todoId: number) =>
atom(false) // 各todoIdに対して boolean型の atom(false) を生成
);

function TodoItem({ todo }: { todo: Todo }) {
// todoCompletedAtomFamily に todo.id を渡して、このアイテム専用の completed Atomを取得
const [isCompleted, setIsCompleted] = useAtom(todoCompletedAtomFamily(todo.id));

return (

  • setIsCompleted(prev => !prev)}
    />

    {todo.text} (ID: {todo.id})
  • );
    }

    function TodoList() {
    const [todos] = useAtom(todosAtom);

    return (

      {todos.map(todo => (

      ))}

    );
    }
    “`

    この例では、todoCompletedAtomFamilyが、個々のToDoアイテムのIDをキーとして、そのアイテムの完了状態を管理するAtomを動的に生成します。各TodoItemコンポーネントは、自身のtodo.idを使って対応するAtomを取得し、その状態を独立して操作できます。これにより、リスト内の他のアイテムの状態に影響を与えることなく、個別のアイテムの状態を管理できます。

    atomFamilyで生成されたAtomは、それが参照されなくなると自動的にメモリから解放されるよう設計されています(ただし、詳細な挙動や設定はJotaiのバージョンや設定に依存する場合があります)。

    その他のユーティリティアトム

    Jotaiには、特定ユースケースに便利なユーティリティアトムがいくつか用意されています (jotai/utils からインポート)。

    • atomWithStorage: localStorageやsessionStorageなど、ブラウザのストレージと同期するAtomを簡単に作成できます。
    • atomWithReducer: ReduxのReducerのようなパターンで状態を管理したい場合に便利です。
    • atomWithReset: 値を初期状態にリセットできるAtomを作成します。
    • splitAtom: 配列のアトムを、その要素ごとのアトムの配列に変換します。リスト表示などで要素ごとに状態を操作したい場合に便利です。

    これらのユーティリティを利用することで、より複雑な状態管理パターンや、外部との連携をシンプルに記述できます。

    パート4:JotaiとReactエコシステムとの連携

    JotaiはReactのために設計されており、Reactの機能や他のライブラリとの連携もスムーズに行えます。

    React.memo と useCallback

    Atomの値が更新されると、そのAtomまたは派生Atomを利用しているコンポーネントが再レンダリングされます。これはJotaiの設計上自然な挙動ですが、不要な再レンダリングは避けるべきです。Reactのパフォーマンス最適化手法であるReact.memouseCallbackは、Jotaiと組み合わせることで効果を発揮します。

    特に、Atomの値がオブジェクトや配列などの参照型である場合、値が更新されなくてもオブジェクトの参照が変わるだけでuseAtomが新しい値を返したとみなされ、コンポーネントが再レンダリングされることがあります。このような場合、子コンポーネントをReact.memoでメモ化しておき、Propsとして渡すコールバック関数をuseCallbackでメモ化することで、親コンポーネントのAtom更新による子コンポーネントの不要な再レンダリングを防ぐことができます。

    また、JotaiのuseAtomValueや派生アトムは、値の変更を非常に効率的に検知するため、Context APIで発生しがちな「値全体が変わったから全部再レンダリング」という問題を避け、必要な部分だけを再レンダリングします。これはJotaiの設計上の大きな利点の一つです。

    TypeScript サポート

    JotaiはTypeScriptで書かれており、TypeScriptとの親和性が非常に高いです。Atomを定義する際に型を指定することで、値の読み書き時に強力な型補完と型チェックの恩恵を受けられます。

    “`typescript jsx
    import { atom, useAtom } from ‘jotai’;

    // 明示的に型を指定
    const countAtom = atom(0);

    interface User {
    id: number;
    name: string;
    email: string;
    }

    // interfaceを使ってオブジェクトの型を指定
    const userAtom = atom(null); // 初期値はnull、ユーザーがロードされたらUser型

    function UserProfile() {
    const [user, setUser] = useAtom(userAtom);

    // userがnullでない場合にのみプロパティにアクセスできる
    if (!user) return

    Please load user

    ;

    // user.name や user.email にアクセスする際に型チェックが働く
    return (

    {user.name}

    {user.email}

    );
    }
    “`

    派生アトムでも型推論が強力に働くため、複雑な状態の変換や組み合わせでも型安全性を保つことができます。

    “`typescript jsx
    const usersAtom = atom([]); // ユーザーリストのアトム

    // ユーザーリストの数を数える派生アトム(型推論により number となる)
    const userCountAtom = atom((get) => get(usersAtom).length);

    // 特定のユーザーを検索する派生アトム(型推論により User | undefined となる)
    const findUserByIdAtom = atom((get) => (id: number) => {
    return get(usersAtom).find(user => user.id === id);
    });

    function UserCounter() {
    const userCount = useAtomValue(userCountAtom);
    return

    Total users: {userCount}

    ;
    }
    “`

    このように、JotaiはTypeScript環境での開発において、状態の定義から利用まで一貫した型安全性を提供し、開発効率とコードの信頼性を向上させます。

    開発者ツール

    Jotaiには公式の開発者ツール「Jotai DevTools」があります。これはRedux DevToolsのように、Atomの現在の値を確認したり、Atomの更新履歴を追跡したり、時間旅行デバッグ(Stateの巻き戻し/再生)を行ったりする機能を提供します。

    “`jsx
    import { Provider } from ‘jotai’;
    import { DevTools } from ‘jotai-devtools’;
    import App from ‘./App’;

    ReactDOM.render(

    {/ Providerの下、アプリケーションコンポーネントの上に配置 /}

    ,
    document.getElementById(‘root’)
    );
    “`

    DevToolsを利用することで、アプリケーションの複雑な状態の流れや、Atom間の依存関係を視覚的に把握しやすくなり、デバッグ作業が効率化されます。

    テスト

    Jotaiを使用したコンポーネントのテストは比較的容易です。useAtomuseAtomValueなどのフックは、React Testing Libraryのようなツールを使ってコンポーネントをレンダリングし、実際のAtomを使ってテストできます。

    あるいは、テスト用にAtomをモックしたり、Providerを使って特定のAtomの初期値をオーバーライドしたりすることも可能です。

    “`javascript
    import { atom, useAtomValue } from ‘jotai’;
    import { Provider } from ‘jotai’;
    import { render } from ‘@testing-library/react’;

    const greetingAtom = atom(‘Hello’);

    function GreetingDisplay() {
    const greeting = useAtomValue(greetingAtom);
    return

    {greeting}

    ;
    }

    test(‘displays default greeting’, () => {
    const { getByText } = render(
    {/ テスト用のProvider /}


    );
    expect(getByText(‘Hello’)).toBeInTheDocument();
    });

    test(‘displays overridden greeting’, () => {
    const overriddenGreetingAtom = atom(‘Hi’); // テスト用に別のAtomを作成
    const { getByText } = render(
    {/ 特定のAtomをオーバーライド /}


    );
    expect(getByText(‘Bonjour’)).toBeInTheDocument();
    });
    “`

    ProviderinitialValuesプロパティを使うことで、テスト対象のコンポーネントが利用するAtomに、テスト用の初期値を注入できます。これにより、外部API呼び出しなどを含む非同期アトムのテストや、特定の状態をシミュレートしたテストが容易になります。

    パート5:Jotaiの活用例

    これまでに説明した概念を踏まえ、Jotaiが実際のアプリケーション開発でどのように活用できるか、いくつかの具体的な例を見てみましょう。

    例1:シンプルなカウンター(再掲と詳細)

    基本ですが、Jotaiのシンプルさをよく表しています。

    “`jsx
    // atoms/counter.ts
    import { atom } from ‘jotai’;

    export const countAtom = atom(0);
    export const doubleCountAtom = atom((get) => get(countAtom) * 2);

    // components/Counter.tsx
    import React from ‘react’;
    import { useAtom, useAtomValue, useSetAtom } from ‘jotai’;
    import { countAtom, doubleCountAtom } from ‘../atoms/counter’; // Atomをインポート

    function CounterDisplay() {
    const count = useAtomValue(countAtom); // 値だけ読む
    const doubleCount = useAtomValue(doubleCountAtom); // 派生アトムの値を読む
    return (

    Count: {count}

    Double: {doubleCount}

    );
    }

    function CounterButtons() {
    const setCount = useSetAtom(countAtom); // セッターだけ読む
    return (


    );
    }

    function CounterApp() {
    return (
    <>



    );
    }

    export default CounterApp;

    // App.tsx (Root)
    import { Provider } from ‘jotai’;
    import CounterApp from ‘./components/Counter’;

    function App() {
    return (



    );
    }
    “`

    この例では、状態の定義(countAtom, doubleCountAtom)がatomsディレクトリに分離されています。コンポーネントは、必要なAtomだけをインポートして利用します。CounterDisplayは値のみ、CounterButtonsはセッターのみを利用することで、それぞれの関心事が明確になり、不要な再レンダリングも抑制されます。

    例2:ToDoリスト(フィルタリング機能付き)

    ToDoリストは、アイテムごとの状態管理と、全体の状態に基づいた派生状態(フィルタリングされたリスト、未完了タスク数など)の管理が必要となる良い例です。

    “`jsx
    // atoms/todos.ts
    import { atom } from ‘jotai’;
    import { atomFamily } from ‘jotai/utils’;

    type Todo = { id: number; text: string; completed: boolean };
    type Filter = ‘all’ | ‘active’ | ‘completed’;

    // 全ToDoアイテムのリストを保持するアトム
    export const todosAtom = atom([]);

    // 現在のフィルター設定を保持するアトム
    export const filterAtom = atom(‘all’);

    // フィルターされたToDoリストの派生アトム
    export const filteredTodosAtom = atom((get) => {
    const todos = get(todosAtom);
    const filter = get(filterAtom);

    switch (filter) {
    case ‘active’:
    return todos.filter((todo) => !todo.completed);
    case ‘completed’:
    return todos.filter((todo) => todo.completed);
    case ‘all’:
    default:
    return todos;
    }
    });

    // 未完了タスク数の派生アトム
    export const activeTodoCountAtom = atom((get) => {
    return get(todosAtom).filter((todo) => !todo.completed).length;
    });

    // TodoのIDをキーとする、個別の完了状態を管理するアトムファミリー
    export const todoCompletedAtomFamily = atomFamily((todoId: number) => atom(false));

    // components/TodoApp.tsx
    import React, { useState, useRef } from ‘react’;
    import { useAtom, useAtomValue, useSetAtom } from ‘jotai’;
    import { todosAtom, filterAtom, filteredTodosAtom, activeTodoCountAtom, todoCompletedAtomFamily } from ‘../atoms/todos’;

    // ToDo入力フォーム
    function TodoInput() {
    const setTodos = useSetAtom(todosAtom);
    const [text, setText] = useState(”);
    const nextId = useRef(0); // 簡単なID管理

    const addTodo = () => {
    if (text.trim()) {
    setTodos(prev => […prev, { id: nextId.current++, text, completed: false }]);
    setText(”);
    }
    };

    const handleKeyPress = (e: React.KeyboardEvent) => {
    if (e.key === ‘Enter’) {
    addTodo();
    }
    };

    return (

    setText(e.target.value)} onKeyPress={handleKeyPress} />

    );
    }

    // 個別ToDoアイテムコンポーネント
    function TodoItem({ todoId, text }: { todoId: number; text: string }) {
    // アトムファミリーから個別のcompleted状態を取得
    const [isCompleted, setIsCompleted] = useAtom(todoCompletedAtomFamily(todoId));

    return (

  • setIsCompleted(prev => !prev)}
    />

    {text}
  • );
    }

    // ToDoリストコンポーネント
    function TodoList() {
    // フィルターされたToDoリストを取得
    const filteredTodos = useAtomValue(filteredTodosAtom);

    return (

      {/ filteredTodos は Todo 型の配列 /}
      {filteredTodos.map(todo => (
      // TodoItem には todo.id と text を渡す

      ))}

    );
    }

    // フィルターボタンコンポーネント
    function TodoFilters() {
    const [filter, setFilter] = useAtom(filterAtom);
    const activeCount = useAtomValue(activeTodoCountAtom); // 未完了タスク数

    return (



    );
    }

    // 全体アプリコンポーネント
    function TodoApp() {
    return (

    Jotai Todo App



    );
    }

    export default TodoApp;

    // App.tsx (Root)
    import { Provider } from ‘jotai’;
    import TodoApp from ‘./components/TodoApp’;

    function App() {
    return (



    );
    }
    “`

    この例では、todosAtom(全リスト)、filterAtom(現在のフィルター)、todoCompletedAtomFamily(個別の完了状態)という3つの独立したAtomを定義しています。filteredTodosAtomactiveTodoCountAtomは、これらのAtomを組み合わせて計算される派生アトムです。

    UIコンポーネントは、必要なAtomや派生アトムだけをuseAtomまたはuseAtomValueで利用します。例えば、TodoItemtodoCompletedAtomFamily(todoId)という特定のAtomファミリーインスタンスだけに関心を持ちます。TodoFiltersfilterAtomactiveTodoCountAtomに関心があります。

    これにより、以下のようなメリットが得られます。

    • 関心の分離: 各Atomは特定の状態のみを管理し、各コンポーネントは必要なAtomのみに依存します。
    • 効率的な更新: 例えば、一つのToDoアイテムの完了状態が変更されても、再レンダリングされるのはそのTodoItemコンポーネントと、activeTodoCountAtomfilteredTodosAtomを利用しているコンポーネントだけです。リスト全体が再レンダリングされるわけではありません。
    • 明確な依存関係: どのAtomがどのAtomに依存しているかが、コード上で明確に定義されます(特に派生アトム)。

    例3:非同期データのフェッチと表示

    非同期アトムを使ったデータフェッチの例です。

    “`jsx
    // atoms/data.ts
    import { atom } from ‘jotai’;

    interface Post {
    userId: number;
    id: number;
    title: string;
    body: string;
    }

    // 投稿リストをフェッチする非同期アトム
    export const postsAtom = atom(async () => {
    const response = await fetch(‘https://jsonplaceholder.typicode.com/posts’);
    if (!response.ok) throw new Error(‘Failed to fetch posts’);
    return response.json();
    });

    // components/PostList.tsx
    import React, { Suspense } from ‘react’;
    import { useAtomValue } from ‘jotai’;
    import { postsAtom } from ‘../atoms/data’;

    function PostListContent() {
    // 非同期アトムの値を Suspense の中で読む
    const posts = useAtomValue(postsAtom);

    return (

      {posts.map(post => (

    • {post.title}

      {post.body.substring(0, 100)}…

    • ))}

    );
    }

    function PostList() {
    return (

    Posts

    Loading posts…\

    }> {/ Suspense で囲む /}

    );
    }

    export default PostList;

    // App.tsx (Root)
    import { Provider } from ‘jotai’;
    import PostList from ‘./components/PostList’;

    function App() {
    return (



    );
    }
    “`

    postsAtomはPromiseを返す関数として定義されているため、これは非同期アトムです。PostListContentコンポーネントは、この非同期アトムの値をuseAtomValueで読み込みますが、親のPostListコンポーネントがSuspenseで囲んでいるため、Promiseが解決されるまでfallbackが表示されます。データ取得ロジックがAtomの定義の中に閉じ込められており、コンポーネントは単にその値を利用するだけという、スッキリとしたコードになっています。

    手動でのローディング/エラー状態管理が必要な場合は、前述のようにuseAtomの第三引数を利用します。

    これらの例から分かるように、Jotaiは様々な状態管理のパターンに対して、Atomという単一の概念と少数のフックを組み合わせることで対応できます。ボイラープレートが少なく、関連するロジックが近くに配置されるため、コードが読みやすく、メンテナンスしやすくなります。

    パート6:Jotaiの哲学と利点、そして他のライブラリとの比較(再訪)

    Jotaiの基本的な使い方と応用例を見てきました。ここで改めて、Jotaiがなぜ状態管理を変革すると言われるのか、その哲学と具体的な利点を深掘りし、既存ライブラリと比較してみましょう。

    Jotaiの哲学:「プリミティブベース」「ボトムアップ」

    Jotaiの中心的な哲学は「Atomは状態のプリミティブである」という点です。これは、アプリケーションの状態を、小さく独立した多数のAtomの集合体として捉えることを意味します。

    • プリミティブベース: String, Number, Booleanなどの言語プリミティブのように、Atomもまた状態という概念における基本的な構成要素です。これらのプリミティブを組み合わせて、より複雑な構造体(派生アトム)を構築します。
    • ボトムアップ: 従来のReduxのような「アプリケーション全体の状態を一つのツリー構造として定義し、それを各コンポーネントに配布する」というトップダウンのアプローチに対し、Jotaiは「必要な場所で必要なAtomを定義し、それらを積み重ねて全体を構築する」というボトムアップのアプローチを推奨します。

    この哲学がもたらす具体的な利点は以下の通りです。

    1. シンプルさと学習曲線: 最小限のAPIと「全てはAtom」というシンプルな概念により、学習コストが極めて低いです。React Hooksに慣れていれば、すぐにJotaiを使い始めることができます。
    2. ボイラープレートの削減: Actionタイプ、Action Creator、Reducer、Selectorなどの定義が不要になり、状態の定義と更新ロジックをAtom内に直接、あるいは関連する派生アトムとして記述できるため、コード量が大幅に削減されます。
    3. 柔軟性: グローバル状態、ローカル状態に近いスコープの状態、派生状態、非同期状態など、様々な種類の状態を同じatomという概念で統一的に扱えます。必要に応じてAtomを定義し、組み合わせるだけで対応できます。
    4. 優れたパフォーマンス: JotaiはAtom間の依存関係を効率的に追跡します。Atomの値が更新された場合、そのAtomに直接的・間接的に依存しているコンポーネントだけが再レンダリングされます。Context APIのような広範囲な再レンダリングを引き起こしにくい設計です。特に派生アトムは計算結果をメモ化するため、依存元のAtomが変わらない限り再計算されません。
    5. TypeScriptとの高い親和性: TypeScriptで書かれており、強力な型推論と型安全性を最初から考慮して設計されています。複雑な派生アトムでも型が追いやすく、大規模開発での信頼性を高めます。
    6. デバッグ体験: Jotai DevToolsを利用することで、Atomの状態遷移を視覚的に追跡できます。Atom間の依存関係を理解すれば、状態がどのように変化し、なぜ特定のコンポーネントが再レンダリングされたのかを把握しやすくなります。

    既存ライブラリとの比較(再訪)

    Jotaiが他の主要なReact状態管理ライブラリとどのように異なるのか、それぞれの特徴を対比させてみましょう。

    特徴 Jotai Context API Redux Recoil
    哲学/概念 プリミティブベース(Atom)、ボトムアップ ツリーベース、トップダウン 一元ストア、予測可能な状態、Fluxアーキテクチャ アトム/セレクター、グラフベース、React的
    状態の単位 小さな独立したAtom Provider毎のまとまり(Context Value) 一つの大きなストアオブジェクト Atom(最小単位)、Selector(派生)
    API atom, useAtom など少数のフック createContext, Provider, useContext createStore, Reducer, Action, useSelector, useDispatch (react-redux) atom, selector, useRecoilState などフック
    ボイラーPL 少ない 比較的少ないがProvider/Consumerが増える 多い (Redux Toolkit で大幅改善) 少ない
    学習曲線 低い(Hooksに慣れていれば) 低い 高い(概念理解、Middlewareなど) 中程度(Atom/Selector理解)
    パフォーマンス きめ細かい更新、依存追跡に優れる Context Value全体に依存、再レンダリング広い Selectorによる最適化が必要 きめ細かい更新、依存追跡に優れる
    TypeScript 高い親和性、強力な型推論 Context Valueに型付け Store, Action, Reducerなど全体に型付け必要 比較的高い親和性
    Provider 推奨(テストやスコープ管理のため) 必須 必須 (Provider from react-redux) 必須 (RecoilRoot)
    キー管理 不要(オブジェクト参照ベース) 不要 Store構造による 必須(グローバルユニークなキー)
    デバッグ Jotai DevTools React DevTools (Contextタブ) Redux DevTools (強力) Recoil DevTools (実験的)
    • Context API との比較: JotaiはContext APIを内部で利用していますが、そのAPIはContextの直接的な使用よりもはるかに宣言的で、きめ細かい更新が可能です。Context APIのパフォーマンス課題やContext Hellを回避できます。シンプルな親子間の状態共有や、深いProps Drillingの回避にはContextも有効ですが、アプリケーション全体や複雑な状態にはJotaiが優位です。
    • Redux との比較: Reduxは依然として大規模で厳格な状態管理が必要なケース、強力なMiddlewareエコシステムを活用したいケース、あるいは既にチームがReduxに習熟している場合には良い選択肢です。しかし、Reduxのボイラープレートや学習コストは無視できません。Jotaiは、よりReact的でシンプル、柔軟なAPIを提供し、多くの場合でReduxと同等以上の機能を、より少ないコードと低い学習コストで実現できます。特に、Redux Toolkitが登場してボイラープレートが減ったとはいえ、Jotaiのシンプルさは際立ちます。
    • Recoil との比較: JotaiとRecoilは設計思想が非常に似ていますが、JotaiはAtomにグローバルなキーが不要な点、よりTypeScriptフレンドリーな設計、APIのシンプルさなどで異なります。JotaiはRecoilのアイデアをさらに洗練させたものと言えるでしょう。どちらを選ぶかは個人の好みやチームの経験にも依存しますが、Jotaiは特にTypeScriptを重視する開発者にとって魅力的な選択肢です。

    結論として、Jotaiは、Reduxの強力な機能とContext APIのシンプルさの良いとこ取りをしつつ、独自の「プリミティブベース」という哲学で状態管理に新たな視点をもたらします。ボイラープレートが少なく、柔軟性が高く、パフォーマンスにも優れており、特にHooksとTypeScriptを積極的に利用している現代のReact開発スタイルと非常に相性が良いです。

    パート7:Jotai導入における考慮事項と注意点

    Jotaiは多くの利点を提供しますが、導入する前に考慮すべき点や注意点もあります。

    学習曲線(高度な機能の場合)

    Jotaiの基本的なAPI(atom, useAtomなど)は非常にシンプルで習得が容易です。しかし、派生アトムの複雑な組み合わせ、非同期アトムのハンドリングパターン、アトムファミリーのメモリ管理など、高度な機能を使いこなすには、Jotai独自の依存関係の解決や更新伝播の仕組みを理解する必要があります。これは、ReduxのMiddlewareやSaga、Contextのパフォーマンス最適化手法などを学ぶほど難しくはないかもしれませんが、全く学習が不要というわけではありません。

    デバッグの複雑さ(依存関係の追跡)

    多数の小さなAtomや複雑な派生アトムが相互に依存し合っている場合、あるAtomの値がなぜ変わったのか、その変更がどのAtomに影響を与え、最終的にどのコンポーネントが再レンダリングされたのかを追跡するのが、Reduxのような一元ストアのデバッグに比べて最初は難しく感じるかもしれません。Jotai DevToolsが強力な助けとなりますが、それでもAtom間の依存関係グラフを頭の中で整理するスキルは必要になります。

    コミュニティとエコシステム

    Jotaiのコミュニティは急速に成長していますが、Reduxのような長年の歴史を持つライブラリに比べると、まだ情報量やサードパーティ製の拡張ライブラリは少ないかもしれません。特定の高度な要件(例: SSRにおける複雑な状態のhydrating、特定の永続化要件など)に対して、解決策を見つけるのに少し労力がかかる可能性があります。ただし、主要なユースケースに対応するためのユーティリティ(jotai/utilsなど)は充実してきています。

    Providerの管理

    デフォルトのグローバルストアで多くのユースケースはカバーできますが、テスト容易性や、特定のサブツリーで独立した状態を持ちたい場合などは、Providerを明示的に使用し、そのスコープを管理する必要があります。Context APIほどProviderのネストが問題になることは少ないですが、アプリケーション設計によってはProviderの配置戦略を考慮する必要があります。

    いつJotaiを使うべきか、そうでないか

    Jotaiは多くのReactアプリケーションに適していますが、万能ではありません。

    • Jotaiが特に輝くケース:
      • HooksとTypeScriptを積極的に使っているプロジェクト。
      • Context APIのパフォーマンスや管理に課題を感じているが、Reduxほどの厳格さやボイラープレートは避けたい場合。
      • アプリケーションの状態が、比較的小さく独立した単位の集まりとして捉えられる場合。
      • 派生状態や非同期状態をシンプルに扱いたい場合。
      • 高速な開発イテレーションを重視する場合。
    • Jotai以外の選択肢を検討すべきケース:
      • アプリケーションが非常に大規模かつ複雑で、状態の一元管理、厳格なルール、予測可能性を最優先する場合(Reduxが依然として有力な選択肢になり得ます)。
      • 既にチーム全体がReduxや別のライブラリに深く習熟しており、移行コストが大きい場合。
      • 状態管理が非常にシンプルで、Context APIやHooksだけで十分な場合。

    多くのモダンなReactアプリケーション、特にミドルサイズまでのアプリケーションや、スタートアップ段階での開発においては、Jotaiは非常に強力で魅力的な選択肢となるでしょう。

    パート8:Jotaiを始めてみよう!

    Jotaiの魅力と使い方が分かったところで、実際にプロジェクトに導入して使ってみましょう。

    インストール

    npmまたはyarnを使って、Jotaiをプロジェクトに追加します。

    “`bash
    npm install jotai

    or

    yarn add jotai
    “`

    TypeScriptを使う場合は、特別な型定義ライブラリは不要です。Jotai自身がTypeScriptで書かれています。

    基本的なセットアップ

    通常は、アプリケーションのルートコンポーネントをjotaiからインポートしたProviderで囲みます。

    “`jsx
    // src/index.tsx または src/main.tsx
    import React from ‘react’;
    import ReactDOM from ‘react-dom/client’; // React 18 の場合
    import { Provider } from ‘jotai’;
    import App from ‘./App’;
    import ‘./index.css’;

    const root = ReactDOM.createRoot(
    document.getElementById(‘root’) as HTMLElement
    );
    root.render(

    {/ ここにProviderを配置 /}



    );
    “`

    <Provider>を配置することで、Jotaiが管理するAtomの状態がコンポーネントツリー内で共有可能になります。前述の通り、デフォルトのグローバルストアを使う場合はProviderは必須ではありませんが、テストやスコープ管理の観点から明示的に配置することが推奨されます。

    最初のAtomを作成・利用する

    “`jsx
    // src/atoms.ts (または任意の場所に)
    import { atom } from ‘jotai’;

    // シンプルな文字列アトム
    export const greetingAtom = atom(‘Hello, Jotai!’);

    // src/components/GreetingDisplay.tsx
    import React from ‘react’;
    import { useAtomValue } from ‘jotai’; // 値だけ読みたいので useAtomValue を使う
    import { greetingAtom } from ‘../atoms’; // 作成したアトムをインポート

    function GreetingDisplay() {
    const greeting = useAtomValue(greetingAtom); // アトムの値を取得
    return (

    {greeting}

    );
    }

    export default GreetingDisplay;

    // src/App.tsx
    import React from ‘react’;
    import GreetingDisplay from ‘./components/GreetingDisplay’;

    function App() {
    return (

    );
    }

    export default App;
    “`

    これで、greetingAtomというAtomの状態をGreetingDisplayコンポーネントで表示するシンプルなアプリケーションが完成です。状態管理のためにこれだけのコードで済むのは、Jotaiの大きな魅力です。

    次に、このAtomを更新するコンポーネントを追加してみましょう。

    “`jsx
    // src/components/GreetingChanger.tsx
    import React from ‘react’;
    import { useSetAtom } from ‘jotai’; // セッターだけ読みたいので useSetAtom を使う
    import { greetingAtom } from ‘../atoms’; // 作成したアトムをインポート

    function GreetingChanger() {
    const setGreeting = useSetAtom(greetingAtom); // アトムのセッターを取得
    const [inputValue, setInputValue] = React.useState(”);

    const handleChange = () => {
    setGreeting(inputValue); // アトムの値を更新
    };

    return (

    setInputValue(e.target.value)}
    placeholder=”Enter new greeting”
    />

    );
    }

    export default GreetingChanger;

    // src/App.tsx を更新
    import React from ‘react’;
    import GreetingDisplay from ‘./components/GreetingDisplay’;
    import GreetingChanger from ‘./components/GreetingChanger’;

    function App() {
    return (


    );
    }

    export default App;
    “`

    これで、GreetingChangerで入力されたテキストがgreetingAtomに設定され、同じAtomを参照しているGreetingDisplayが自動的に更新されて新しい挨拶が表示されるようになります。GreetingChangerはAtomのセッターのみを使い、GreetingDisplayは値のみを使うことで、両方のコンポーネントが不要な再レンダリングを避けています。

    このように、Jotaiでは状態の定義と利用、更新が非常に直感的かつ最小限のコードで実現できます。

    まとめ:Jotaiが拓く状態管理の未来

    本記事では、Reactにおける従来の状態管理手法の課題を踏まえ、Jotaiがどのようにそれらを解決し、状態管理のあり方を変革しうるのかを詳細に見てきました。

    Jotaiの「プリミティブベース」という哲学は、状態を小さなAtomとして定義し、それらを組み合わせて複雑な状態やロジックを構築するという、シンプルかつ強力なアプローチを提供します。ReduxのようなボイラープレートやContext APIのパフォーマンス問題を避けつつ、ReactのHooksやSuspense、TypeScriptといったモダンなエコシステムとの高い親和性を持っています。

    派生アトムによる効率的な計算値管理、非同期アトムによるデータフェッチのシンプル化、アトムファミリーによる動的なリストアイテム状態管理など、Jotaiは様々なユースケースに対応できる柔軟性を持っています。

    Jotaiはまだ比較的新しいライブラリですが、その設計思想と開発者体験の良さから急速にコミュニティでの支持を集めています。多くのプロジェクトで状態管理の課題を解決し、開発効率とコードの品質を向上させる可能性を秘めています。

    もしあなたがProps Drilling、Context Hell、Reduxの複雑さに悩んだ経験があるなら、ぜひ一度Jotaiを試してみてください。Atomという新しい状態のプリミティブは、あなたのReactアプリケーションにおける状態管理の考え方を根本から変えるかもしれません。シンプルで柔軟、そしてパワフルなJotaiが、あなたの開発体験をより良いものにすることでしょう。

    React開発者必見のJotai、この機会にぜひ習得し、あなたのプロジェクトに活かしてみてください。きっとそのシンプルさと柔軟性に驚くはずです。


    コメントする

    メールアドレスが公開されることはありません。 が付いている欄は必須項目です

    上部へスクロール