【React】状態管理をRecoilを使って行うチュートリアル
今回はReactの状態管理をRecoilを使って行う方法について説明します。Recoilはまだ実験段階のライブラリですが、Zennなどいくつかのサービスで利用されはじめています。
前提条件は以下
- VSCode上でcreat react appを使ってReact、TypeScriptの環境で行う
- ESLint(Airbnb)とPrettierを追加して、VSCode上の拡張機能と連動させる
環境は以下です。
- macOS Catalina v10.15.5
- Visual Studio Code v1.57.0
- React v17.0.2
- Recoil v0.6.1
- TypeScript v4.5.5
- node.js v16.13.1
Recoilを説明するためのデモ
今回のチュートリアルでは以下のアプリを作りながら説明します。
input部分にテキストを入力して「登録」ボタンをクリックすると、テキストが表示されていきます。表示されたテキストの数は「○個のタスクがあります」部分に反映されます。
シンプルな機能なのでRecoilの使い方もわかりやすいと思います。
VSCodeで開発環境を作る
開発環境は以下の記事に沿って構築しています。
この開発環境に追加する形でRecoilをインストールしていきます。
Recoilのインストール
Recoil本体とTypeScriptで使う型をインストールします。
npm i --save recoil @types/recoil
Recoilの詳細については以下をどうぞ。
ディレクトリの構成について
説明するディレクトリ構成は以下です。
状態の管理はcomponentsの中にあるstatesディレクトリで行います。
|_public/
| |- index.html
|
|_src/
|- assets/ (共通で使うCSSや画像をまとめている)
| |- css/
| | |- _base.scss
| | |- _custom-property.scss
| | |- styles.scss
| |
| |- img/
|
|- components/(コンポーネントをまとめている)
| |- blocks/(ページを構成するコンポーネント)
| | |- AddBlock.module.scss
| | |- AddBlock.tsx
| | |- InputBlock.module.scss
| | |- InputBlock.tsx
| |
| |- pages/(全ページを管理する)
| | |- home/(トップページ)
| | | |- Page.module.scss
| | | |- Page.tsx
| |
| |- types/(共通で使う型をまとめている)
| | |- items.ts
| |
| |- states/(状態を管理する)
| |- addTextState.ts
| |- inputTextState.ts
|
|- App.tsx
|- index.tsx
typesディレクトリにあるitems.tsにはアプリケーション全体で使用する型を書きます。今回は以下だけです。
export type Items = {
id: string;
text: string;
};
input要素に入力したテキストの状態を管理する
input要素に入力したテキストとは、以下に書いた部分になります。
入力したテキストの状態管理をRecoilに書くと以下です。
statesディレクトリのinputTextState.tsに書いています。
import { atom } from 'recoil';
export const inputTextState = atom<string>({
key: 'inputTextState',
default: '',
});
Recoilでは状態管理をする場合、atomを宣言して使用します。必ず必要なプロパティはkeyとdefaultです。
- key … 他とかぶらない文字列
- default … 状態の初期値
inputの部分について、最初は何もテキストを入力していない状態なのでdefaultには空文字を指定しています。
次に実際にコンポーネントで使う場合について説明します。
input要素のコンポーネントに「状態」使う
以下のinput要素のコンポーネントで実際にRecoilを使って状態を反映させます。
blocksディレクトリのInputBlock.tsxに以下のコードを書いています。
import { useCallback } from 'react';
import { useRecoilValue} from 'recoil';
import { inputTextState } from '../states/inputTextState';
import classes from './InputBlock.module.scss';
export const InputBlock = () => {
const inputText = useRecoilValue(inputTextState);
const setInputText = useSetRecoilState(inputTextState);
const onChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setInputText(event.target.value);
},
[setInputText]
);
return (
<div className={classes.block}>
<input type="text" value={inputText} onChange={onChange} className={classes.input} />
</div>
);
};
Recoilでは今の状態を取得するにはuseRecoilValueを使い、状態を変更するときはuseSetRecoilStateを使います。
- useRecoilValue … 今の状態を取得する
- useSetRecoilState … 状態を変更する
各々を変数に代入してしまえばあとはuseStateと使い方は同じです。
登録したタスクの状態を管理する
input要素でテキストを入力したあと、「登録」ボタンをクリックするとタスクが追加され、タスクの個数も表示されます。
statesディレクトリのaddTextState.tsに書いています。
import { atom, selector } from 'recoil';
import { Items } from '../types/items';
export const addTextState = atom<Array<Items>>({
key: 'addTextState',
default: [],
});
export const addTextStateLength = selector<number>({
key: 'addTextStateLength',
get: ({ get }) => {
const addTextNumber: Array<Items> = get(addTextState);
return addTextNumber?.length || 0;
},
});
atomの部分では「登録」ボタンを押したときに、タスクを追加していきたいのでdefaltには配列を指定します。
atomは状態を管理するものですが、9行目のselectorは状態を取得して加工するために使います。今回はgetでaddTextStateの値を取得して、配列の中にいくつ値があるかreturnで返しています。
次に実際にコンポーネントで使う場合について説明します。
input要素のコンポーネントに「状態」を追加
「登録」ボタンを押すことでinput要素に書かれたテキストをタスクとして表示させていきたいのでButton要素を追加して、クリックイベントを設定します。
blocksディレクトリのInputBlock.tsxに追加して書いていきます。
import { useCallback } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { addTextState } from '../states/addTextState';
import { inputTextState } from '../states/inputTextState';
import classes from './InputBlock.module.scss';
const getKey = () => Math.random().toString(32).substring(2); // 0〜1未満の乱数字を取得して、数字を32進法に文字列に変換。前から3番目から文字を抽出
export const InputBlock = () => {
const inputText = useRecoilValue(inputTextState);
const setInputText = useSetRecoilState(inputTextState);
const addText = useRecoilValue(addTextState);
const setAddText = useSetRecoilState(addTextState);
const onChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setInputText(event.target.value);
},
[setInputText]
);
const onClick = () => {
setAddText([...addText, { id: getKey(), text: inputText }]);
setInputText('');
};
return (
<div className={classes.block}>
<input type="text" value={inputText} onChange={onChange} className={classes.input} />
<button type="button" className={classes.button} onClick={onClick}>
登録
</button>
</div>
);
};
状態を取得したいときにuseRecoilValueを使い、状態を変更するときはuseSetRecoilStateを使うことを覚えておきましょう。
タスク表示用のコンポーネントに「状態」を使う
以下のようにタスク用のコンポーネントに状態を反映させます。
blocksディレクトリにあるAddBlock.txに書いていきます。
import { useRecoilValue } from 'recoil';
import { addTextState, addTextStateLength } from '../states/addTextState';
import { Items } from '../types/items';
import classes from './AddBlock.module.scss';
export const AddBlock = () => {
const addText = useRecoilValue(addTextState);
const addTextLength = useRecoilValue(addTextStateLength);
return (
<div className={classes.block}>
<div className={classes.length}>{addTextLength}個のタスクがあります</div>
<ul className={classes.list}>
{addText.map((item: Items) => (
<li key={item.id} className={classes.item}>
{item.text}
</li>
))}
</ul>
</div>
);
};
タスクの個数は8行目でuseRecoilValueから受け取り、11行目で表示させています。
タスク自体は7行目でuseRecoilValueから受け取り、13行目でmapを使ったループで表示させています。
状態をアプリ全体で共有するために
input部分とタスク表示部分のコンポーネントができたので、homeディレクトリの中にあるPage.tsxにそれぞれのコンポーネントを読み込んで1つにまとめます。
import { AddBlock } from '../../../components/blocks/AddBlock';
import { InputBlock } from '../../../components/blocks/InputBlock';
import classes from '../../../components/pages/home/Page.module.scss';
export const Page = () => (
<div className={classes.main}>
<InputBlock />
<AddBlock />
</div>
);
あとはRecoilで定義した状態をアプリ全体で使うために<RecoilRoot>で全体のコンポーネントを囲う必要があります。今回はPage.tsxをApp.tsxに読み込んで使うので、App.tsxは以下のように書けます。
import { RecoilRoot } from 'recoil';
import { Page } from './components/pages/home/Page';
const App = () => (
<RecoilRoot>
<Page />
</RecoilRoot>
);
export default App;
まとめ
今回はReactの状態管理をRecoilを使って行う方法を説明しました。
ざっくりまとめると以下です。
状態はatomを宣言して管理。状態の値を取得する場合はuseRecoilValueを使い、状態を変更したい場合はuseSetRecoilStateを使う。
状態を加工する場合はselectorを宣言し、getを使って状態を取得して何かして加工したらreturnで値を返す。その値を取得したい場合はuseRecoilValueを使う。
アプリ全体で状態を共有する場合はコンポーネントを<RecoilRoot>で囲う必要がある。
---
今回は紹介できませんでしたが、Recoilが持つ便利の機能の中にはatomFamilyやselectorFamilyもあるので覚えておくと良いでしょう。
以下の記事が参考になります。