Recoilで簡単に状態管理を実現しよう
初めに
今回は、状態管理ライブラリのRecoilについて解説します。状態管理ライブラリといえば、Reduxが最も有名かと思いますが、複雑なアーキテクチャーや冗長なコードが必要であると感じる人もいます。一方、Recoilはよりシンプルな設計であるため、Reduxと比較して、学習コストが低くなっています。私自身もReduxの勉強をして、あまり理解できずにいましたが、Recoilは簡単に理解できたので、皆さんに共有したいと思い、この記事を書きました。
使用ライブラリとそのバージョン
- react==18.2.0
- next==13.2.4
- recoil==0.7.7
環境構築
まずは環境構築をします。
# nextjsの雛形を作ります。
npm create next-app study-recoil
cd study-recoil
Recoilをインストールします。
npm install recoil
初期設定
まずはRecoilを使うための初期設定をしましょう。
import "@/styles/globals.css";
import type { AppProps } from "next/app";
import { RecoilRoot } from "recoil";
export default function App({ Component, pageProps }: AppProps) {
return (
<RecoilRoot>
<Component {...pageProps} />
</RecoilRoot>
);
}
_app.tsx
で <Component {...pageProps} />
を<RecoilRoot>
で囲むだけです。
これだけで、useRecoilState
やuseRecoilValue
(後述)などのRecoil Hooksが利用可能になります。
状態を定義
Recoilでは、状態はatom
という概念で表現されます。atom
は、一意のキー(key)と初期値(default)を持つJavaScriptオブジェクトです。
# 状態を管理するためのファイルをsrc/state配下に作成
mkdir src/state
touch src/state/textState.ts
key
は状態の名前であり、アプリケーション内で一意である必要があります。
default
には状態の初期値を設定します。
import { atom } from "recoil";
export const textState = atom({
key: 'textState',
default: '',
});
状態の読み書き
状態を定義できたので、その値を読み書きしてみましょう。
状態を読み書きするためには、useRecoilState
というHookを使用します。
import { useRecoilState } from "recoil";
import { textState } from "../state/textState";
import { ChangeEvent } from "react";
export default function Home() {
const [text, setText] = useRecoilState(textState);
function handleChange(e: ChangeEvent<HTMLInputElement>) {
setText(e.target.value);
}
return (
<div>
<input type="text" value={text} onChange={handleChange} />
<p>You entered: {text}</p>
</div>
);
}
6行目のコードが状態を読み書きするためのHookです。
useRecoilState
の引数には、先ほど作成したatom
を指定します。
Reactを使ったことがある方は「useState
に似ているな」と思ったのではないでしょうか。
使い方自体はuseState
と同じです。
npm run dev
で開発サーバーを起動して確認してみましょう。
defaultの値を空文字にしたので You entered: の後には何も表示されていません。
inputボックスに文字を打ち込めば状態が更新されていることがわかります。
状態の読み取り
useRecoilValue
というのも存在します。これは、useRecoilState
と違い状態のみを取得することができます。
touch src/pages/text.tsx
別のページを作成します。
import { textState } from "../state/textState";
import { useRecoilValue } from "recoil";
import Link from "next/link";
export default function Text() {
const text = useRecoilValue(textState);
return (
<div>
<p>You entered: {text}</p>
<Link href="/">ホームへ</Link>
</div>
);
}
useRecoilValue
の第一引数にもatomを指定します。
画面を遷移するためのLinkも設置しておきました。
// index.tsxからtext.tsxへ移動するためのLinkを設置
<Link href="/">textへ</Link>
画面で確認しましょう。
inputボックスに文字を入力し、画面を遷移してもtext
は保持されたままです。
更新関数のみを取得できるuseSetRecoilState
も存在しますが、この記事では説明を省略します。
計算された状態を定義
Recoilでは、複数のatom
を組み合わせて計算された状態を定義することもできます。
import { atom, selector } from "recoil";
export const charCountState = selector({
key: 'charCountState',
get: ({ get }) => {
const text = get(textState);
return text.length;
},
});
selector
とはatom
と同じような構造のオブジェクトであり、getプロパティに関数を指定して計算された状態を定義できます。
get関数の第一引数には使用したいatom
やselector
を指定します。
今回の場合は先ほど定義したtextState
の文字数をカウントして返すselector
を作成しました。
計算された状態を使用
selector
は状態を返すだけなので、更新関数は存在しません。
よってuseRecoilValue
で値の取得ができます。
import { useRecoilState, useRecoilValue } from "recoil";
import { charCountState, textState } from "../state/textState";
import { ChangeEvent } from "react";
import Link from "next/link";
export default function Home() {
const [text, setText] = useRecoilState(textState);
const charCount = useRecoilValue(charCountState); // 追加
function handleChange(e: ChangeEvent<HTMLInputElement>) {
setText(e.target.value);
}
return (
<div>
<input type="text" value={text} onChange={handleChange} />
<p>You entered: {text}</p>
<p>Character count: {charCount}</p> // 追加
<Link href="/text">textへ</Link>
</div>
);
}
画面で確認しましょう。
この通りinputボックスに入力された値の文字数がカウントされています。
selectorを使用するメリット
selectorの説明をみて「useRecoilState
で取得したstate
をstate.length
とすれば良いのではないか」と感じた人もいると思います。
selector
を使用する主なメリットは、複数のatom
から取得されたデータを元に、より複雑な状態を計算できることです。また、メモ化された関数を提供するため、複数回呼び出される場合でも、最初の呼び出しで計算された結果を返します。さらに、コードをシンプルにし、アプリケーション全体で共有される状態の複雑さを減らすことができます。
TypeScript対応
最後にTypeScriptで型をつけていきましょう。
import { atom, selector } from "recoil";
export const textState = atom<string>({
key: "textState",
default: "",
});
export const charCountState = selector<number>({
key: "charCountState",
get: ({ get }) => {
const text = get(textState);
return text.length;
},
});
atom
にはdefault
の型、selector
にはreturnされる値の型を指定します。
まとめ
以上が、Recoilの基本的な使い方についての説明です。Recoilを使用することで、簡単かつ柔軟な状態管理を実現することができます。また、ReactのHooksを利用するため、学習コストも低くなっています。
始めて記事を書いたので拙い文章だったかもしれませんが、参考になったと思ったら、いいねを押していただけると幸いです。
Discussion
Selectorを使わずに文字列長の計算をカスタムフック経由で実現してみました。
デモコードです。
簡単ですが、以上です。