はじめに
jotaiとは、React用の状態管理ライブラリです。軽量かつシンプルなことが特徴で、Recoilと同じくatomという単位で状態を管理しています。
筆者が自己学習で個人開発を始めた当初、Reactの状態管理は職場でも採用されていたRecoilを使おうとしていたのですが、既にRecoilはライブラリの更新が凍結されていたため、その代用として思想が類似しているjotaiをアプリのモーダル表示に使用しました。いざ使ってみるとよわよわエンジニアの私でも問題なく作業を進めることができたので、せっかくなら記事に残そうと思い、本記事に至ります。
そもそも状態管理とは
使い方を解説する前に、基礎となる状態管理の考え方についておさらいしておこうと思います。
Reactにおいて状態管理とは画面に影響を与える動的なデータのことを指します。具体的な例としては、以下のようなデータです。
- 入力フォームの文字
- ボタンを押した回数
- APIから取得したユーザー情報
- モーダルの開閉フラグ
いずれもユーザーの操作の内容を保持し、それによって画面の表示を変更しなければならないという共通点があります。Reactの思想に「画面(UI)は、アプリが持つ状態(state)から自動的に決まる」というものがあるため、これらの値は適切に状態管理をする必要があるというわけです。
なお、Reactにはstateとcontextという状態管理方法が標準で備わって居ます。この2つは以下のように差別化されます。
stateとcontextの差別化
state……単一コンポーネント内 or 親子間でのシンプルな状態管理
context……複数コンポーネントを跨ぐ状態の管理
ここに更にjotaiのような外部ライブラリも加わります。使いどころとして、複数コンポーネントを跨ぐ状態の管理という点ではcontextと共通しているのですが、以下のような点で差別化することができます。
外部ライブラリとcontextの差別化
外部ライブラリ……複雑または頻繁に更新される状態の管理
- ショッピングカート(大量の商品 + 更新処理)
- リアルタイムデータ(チャット、通知など)
- 大規模アプリで多層に渡る状態共有
context……少量のグローバル状態を配布
- ログイン中のユーザー情報
- ダーク/ライトテーマの設定
- 言語設定
以上でざっくりではありますが、基本的なところはおさらいすることができたと思うので、いよいよ実際の使い方について解説していきたいと思います。
jotaiでモーダルを作成
1. ライブラリのインストール
まずは、以下のコマンドをターミナルで実行し、ライブラリをインストールします。
npm istall jotai
2. 状態(atom)の定義
次に、実際に管理する状態(atom)を以下の例のように定義します。
筆者が作成したモーダルは警告やエラーをユーザーに表示するモーダルであるため、モーダルの開閉以外にも、モーダルが警告表示orエラー表示、表示するメッセージ、警告である場合のボタン選択といった値を状態管理するようにしています。
import { atom } from 'jotai';
export type ModalType = 'error' | 'alert' | null;
// モーダルの開閉を管理
export const isModalOpenAtom = atom(false);
// モーダルに表示するメッセージ
export const modalMessageAtom = atom<string | null>(null);
// 警告ダイアログのボタン押下に応じた値を保持(OK: true or キャンセル: false)
export const modalResolveAtom = atom<((result: boolean | void) => void) | null>(null);
// モーダル種別
export const modalTypeAtom = atom<ModalType>(null);
3. UI部分の作成
その次に、実際に画面に表示するモーダルのUIを作成します。
2で作成したatomと、jotaiからuseAtomをそれぞれインポートし、Reactのstateのような形式で状態管理を行います。
isOpenの値がtrueとなった時にモーダルを表示し、typeの値によってレイアウトを変更するようにしてあります。
ボタンが押下された時には、それらの値を書き換え、resolveからboolean型の値を返却します。
import { useNavigate } from 'react-router-dom';
import { useAtom } from 'jotai';
import {
isModalOpenAtom,
modalMessageAtom,
modalResolveAtom,
modalTypeAtom,
} from '../atoms/modalAtom';
import { messages } from '../constants/message';
import styles from './Modal.module.css';
const Modal = () => {
const navigate = useNavigate();
// useAtomでatomで定義した状態を基に現在の値を参照するための変数とそれを更新するための関数を作成
// イメージとしてはstateが近い
const [isOpen, setIsOpen] = useAtom(isModalOpenAtom);
const [message, setMessage] = useAtom(modalMessageAtom);
const [resolve, setResolve] = useAtom(modalResolveAtom);
const [type, setType] = useAtom(modalTypeAtom);
const close = (result: boolean | void = true) => {
setIsOpen(false);
setMessage(null);
setType(null);
// ユーザーがボタンを押下したタイミングでPromiseを解決する。
// 4で解説するが、カスタムフックとして関数化しているため、この処理が必要
if (resolve) {
resolve(result);
setResolve(null);
}
// 特定のメッセージの場合にだけ強制的にトップ画面に遷移するようにしています。
if (message === messages.ERROR.E001) {
navigate('/');
}
};
// モーダルの開閉状態がfalse,またはモーダルの種類がnullである場合は何も表示しない
if (!isOpen || !type) return null;
// CSSは省略している。
return (
<div>
<div>
<p>{message}</p>
<div>
<button onClick={() => close(true)}>OK</button>
{type === 'alert' && (
<button onClick={() => close(false)}>キャンセル</button>
)}
</div>
</div>
</div>
);
};
export default Modal;
4. カスタムフックの作成
UIに続き、カスタムフックを作成します。
カスタムフックとは「use」で始まる独自の関数を作って、状態管理や処理のロジックを再利用可能にしたもののことを指します。2、3ではモーダルを表示するのに4つの状態を定義しました。しかし、モーダルを表示するためだけにいちいち4つも状態を更新していてはコードは冗長になりますし、面倒ですよね?
そのため、処理を関数のようにまとめることで、どこでも呼び出せるようにしようというわけです。関数との違いは処理の中でuseStateやuseEffectを使用できることにあります。
以下のコードは、筆者が警告を知らせるモーダルを表示する際に使用しているカスタムフックです。
useSetAtomは、3で出てきたuseAtomの更新用関数のみを定義できるようにしたものです。これにより、定義した更新用関数でatomの状態を更新できるようになります。
カスタムフックの処理は以下のようになっています。
カスタムフックの処理
➀表示するメッセージを引数にモーダルを呼び出す。
➁モーダルを閉じる時には、3のUI部分にてresolveの引数として指定した真偽値を呼び出し元に返却する。
※Promiseとなっているのはそのため。
補足
ちなみに、もしエラーモーダルのような閉じる方法に選択肢がない場合はPromiseとなります。
また、setResolveも、「() => resolve()」を設定し、戻り値を返さず処理を即時終了するようになります。
import { useSetAtom } from 'jotai';
import {
isModalOpenAtom,
modalMessageAtom,
modalResolveAtom,
modalTypeAtom,
} from '../../atoms/modalAtom';
export const useAlertModal = () => {
const setIsOpen = useSetAtom(isModalOpenAtom);
const setMessage = useSetAtom(modalMessageAtom);
const setResolve = useSetAtom(modalResolveAtom);
const setType = useSetAtom(modalTypeAtom);
return (message: string): Promise<boolean> =>
// 戻り値がない場合は以下のようになる
// return (message: string): Promise<void> =>
new Promise<boolean>((resolve) => {
setMessage(message);
setType('alert');
setResolve(() => resolve);
// 戻り値がない場合は以下のようになる
// setResolve(() => () => resolve());
setIsOpen(true);
});
};
5. 実際に呼び出してみる
では、実際に呼び出してみます。呼び出し方はこれまでの準備のおかげで簡単にできます。
作成したカスタムフックとuseAtomをインポート。コンポーネント内でカスタムフックを関数として定義。メッセージを引数にモーダルを呼び出す。この3ステップだけで使うことができます。
ここまでくれば、モーダルの作成は完了です。
//インポート
import { useAtom } from 'jotai';
import { useErrorModal } from '../components/Hooks/useErrorModal';
import { useAlertModal } from '../components/Hooks/useAlertModal';
const Memo: React.FC = () => {
//カスタムフックを関数として定義
const showErrorModal = useErrorModal();
const showAlertModal = useAlertModal();
const sampleFunc = async () => {{
await ErrorModal("ここに表示するメッセージ");
// アラートモーダルは真偽値を返すので、それを受け取る
const confirm = await showAlertModal("ここに表示するメッセージ");
};
return(
);
}
EX. Providerについて
最後に、ちょっと番外編です。5番までで基本的なことは抑えられていると思うので、ブラウザバックしていただいても問題ありません。
内容はjotaiにおけるProviderについてです。Providerとは、状態管理におけるスコープのことです。変数の定義でもスコープの考え方がありますが、イメージとしては近いです。Providerを使用することで、独立した状態管理を作成することができます。
使い方は以下のようにインポートしたProviderで独立させたいコンポーネントを囲うことで実現できます。以下のサンプルコードでは、独立したカウンターが2つとそれらの合計のカウンターが1つになります。
また、仮にProviderの設定をしなかった場合は、自動的にグローバルスコープです。
import { Provider, atom, useAtom } from "jotai";
const countAtom = atom(0);
function Counter() {
const [count, setCount] = useAtom(countAtom);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}
export default function App() {
return (
<div>
{/* グローバルの状態を使う */}
<Counter />
{/* 独立した状態スコープ */}
<Provider>
<Counter />
</Provider>
{/* さらに独立したスコープ */}
<Provider>
<Counter />
</Provider>
</div>
);
}
最後に
今回はjotaiについて解説しました。
かなり簡単に扱えるので、特にrecoilの移行先を探している方はjotaiの使用を是非検討してみてください。