【TS】今さら聞けないRecoil
はじめに
今回はRecoil
についての紹介(+自分の学習メモ)です。
Recoil
はFacebook
が開発したReact
向けの状態管理全般を扱うライブラリです。
以下の記事でも触れられていますが、Zenn
の状態管理にも用いられており、今まさにシェアを拡大しているライブラリといえます。
Javascript
での実装例はよく見かけるので、今回はTypescript
を用いて記載した例を紹介していきます。
Recoil
とは
そもそも実装の紹介の前に、Recoil
についてです。
Recoil
はFacebook
より2020年5月に発表された、状態管理ライブラリです。
以下、公式ページになります。
同様に状態管理を行うライブラリとしてはRedux
が頭一つ抜けている印象ですが、実際に使ってみるとかなり違うことがわかります。
とりわけ 「データストアを集約するか否か」 という思想が大きく異なり、その性質がそのまま使い勝手に反映されているイメージです。
また、React Hooks
との親和性もよく、useState
などと同じような記法・使い方をしているため、既にFunctional Component + Hooks
を中心に開発している場合はすんなり理解できるかと思います。
注意点として、現在はまだExperimental
(実験的)なリポジトリとなっています。
今後のサポート等のことを考えると、製品にゴリゴリ組み込むには少し早いかもしれません。
実装サンプル
今回実装したアプリのサンプルは以下になります。
よくあるTODOアプリで、タスクの追加、検索ができます。
バージョン情報
- react@17.0.1
- react-dom@17.0.1
- recoil@0.1.2
実装
以下の手順で実装を行います。
プロジェクトの作成
以下コマンドでプロジェクトを作成します。
プロジェクト名はrecoil-ts-sample
としました。
npx create-react-app recoil-ts-sample --template typescript
cd recoil-ts-sample
Recoilのインストール
Recoil
のインストールを行います。
yarn add recoil
typesの作成
まずこのアプリで扱う「タスク」のtype
を作成します。
ソースとしてはsrc/types/Todo.ts
になります。
今回はシンプルにtitle
だけを持つTodo
型を定義しました。
type Todo = {
title : string;
}
export default Todo;
Atomの作成
続いてAtom
の作成です。
Atom
とはRecoil
におけるデータストアのことで、atom()
を使って宣言します。
atom()
にはAtomOptions
型のオブジェクトを渡します。
AtomOptions
は以下のような型です。
// atom.d.ts
export interface AtomOptions<T> {
key: NodeKey;
default: RecoilValue<T> | Promise<T> | T;
effects_UNSTABLE?: ReadonlyArray<AtomEffect<T>>;
dangerouslyAllowMutability?: boolean;
}
必須なのはkey
とdefault
のみです。
key
はアプリケーション全体で一意となる文字列で、default
はAtom
として状態管理を行いたい値の初期値になります。
例えばアプリ上で表示する「タスク一覧」をAtom
で宣言するには以下のようになります。
import { atom } from "recoil";
import Todo from '../types/Todo';
export const todoListState = atom<Todo[]>({
// keyは"todoList"
key: "todoList",
// 初期値として3件のタスクを持つ配列を宣言
default: [
{title: "one"},
{title: "two"},
{title: "three"},
],
});
同様に画面上部の「追加したいタスク名のフィールド」と「検索文字列のフィールド」の状態をAtom
で宣言します。
import { atom } from "recoil";
export const todoTitleFormState = atom<string>({
key: "todoTitleForm",
default: '',
});
import { atom } from "recoil";
export const searchTextFormState = atom<string>({
key: "searchTextForm",
default: '',
});
Atom
の宣言はRedux
におけるStore
のようなもの、と理解すると分かりがいいかもしれません。
Selectorの作成
同様にSelector
を作成します。
Selector
はAtom
の値を用いて何らかの計算や加工・副作用を処理した結果を返します。
今回のアプリの例では「実際に画面に表示されるタスク一覧」がSelector
を使って取得する値になります。
実際に画面に表示されるタスクは 「タスク全件」の中から「検索フィールドに入力した文字」に該当する文字を含むものだけを表示している ためです。
Selector
の宣言はselector()
で行います。
引数としてReadOnlySelectorOptions
型のオブジェクトを渡す必要があります。
ReadOnlySelectorOptions
型は以下です。
export interface ReadOnlySelectorOptions<T> {
key: string;
get: (opts: { get: GetRecoilValue }) => Promise<T> | RecoilValue<T> | T;
dangerouslyAllowMutability?: boolean;
}
Atom
同様、一意となるkey
を持っています。
さらに「どういう値を取得するか」を定義したget
を持ちます。
get
は引数としてGetRecoilValue
型のget
(プロパティ名が同じなのでややこしいですが・・・)をプロパティに持つオブジェクトを受け取る関数です。
実際に作成したSelector
が以下になります。
import { selector } from "recoil";
import { todoListState } from '../atoms/TodoListAtom';
import { searchTextFormState } from '../atoms/SearchTextFormAtom';
import Todo from '../types/Todo';
export const searchedTodoListSelector = selector<Todo[]>({
key: "searchedTodoListSelector",
// getは{ get }を引数に取る関数
get: ({ get }) => {
// 引数のgetを使ってAtomから最新の値を取得(タスク一覧)
const todoList: Todo[] = get(todoListState);
// 同様に検索フィールドの文字列を取得
const searchText: string = get(searchTextFormState);
// 検索フィールドに入力がある場合は、その条件に合致したタスクのみを返却する
return searchText? todoList.filter((t) => t.title.includes(searchText)) : todoList;
},
});
Rootの宣言
これでRecoil
で状態管理を行う準備が整いました。
Recoil
で状態管理をするためには、状態管理をしたい箇所を<RecoilRoot>
でラップする必要があります。
Redux
でいうところの<Provider>
でラップするのと同じイメージです。
index.tsx
内でRecoilRoot
を使ってApp
をラップするようにしましょう。
import React from "react";
import ReactDOM from "react-dom";
import { RecoilRoot } from "recoil";
import App from "./App";
ReactDOM.render(
<React.StrictMode>
<RecoilRoot>
<App />
</RecoilRoot>
</React.StrictMode>,
document.getElementById("root")
);
// ...以下略
AtomやSelectorから値を取得する
それでは早速コンポーネント内でAtom
やSelector
から値を取得してみましょう。
値を取得するにはuseRecoilValue()
を使います。
useRecoilValue()
には宣言したAtom
やSelector
を引数として渡します。
その返り値としてAtom
やSelector
の最新値が返されます。
例えば「タスク一覧」を表示するTodoList.tsx
は以下のようになります。
import { useRecoilValue } from "recoil";
import { searchedTodoListSelector } from "../selectors/SearchedTodoListSelector";
import Todo from "../types/Todo";
const TodoList: React.FC = () => {
// useRecoilValueにsearchedTodoListSelectorを渡す
// 返される値はsearchedTodoListSelectorのget()で定義した通りTodo[]
const list: Todo[] = useRecoilValue(searchedTodoListSelector);
return (
<div>
<p>タスク一覧</p>
<ul>
{list.map((todo: Todo, i: number) => {
return <li key={`${todo.title}_${i}`}>{todo.title}</li>;
})}
</ul>
</div>
);
};
export default TodoList;
Atomの値を変更する
Redux
においてStore
の値をAction
をDispatch
することで変更すると、参照しているコンポーネントでは再描画が行われました。
Recoil
においてもAtom
の値を変更することで、参照しているSelector
やコンポーネントに自動反映されます。
Atom
の値を変更するためにはuseSetRecoilState()
にAtom
を引数として渡します。
返り値としてSetterOrUpdater<T>
型の関数が返されます。
これはAtom
の値に対するSetter
関数のようなもので、この関数を経由してAtom
を更新することで前述の再計算処理が行われます。
例えば「追加するタスク名を入力するフィールド」のコンポーネントであるTitleForm.tsx
は以下のようになります。
import { useCallback } from "react";
import { useRecoilValue, useSetRecoilState, SetterOrUpdater } from "recoil";
import { todoTitleFormState } from "../atoms/TodoTitleFormAtom";
const TitleForm: React.FC = () => {
// useRecoilValueでtodoTitleFormStateの値を取得
const todoTitleFormValue: string = useRecoilValue(todoTitleFormState);
// useSetRecoilStateでtodoTitleFormStateの値を更新するSetter関数を取得
const setTodoTitleFormValue: SetterOrUpdater<string> = useSetRecoilState(
todoTitleFormState
);
// テキストフィールドのonChange処理
const onChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
// 先に取得したsetTodoTitleFormValueに対して更新したい値を渡して実行
setTodoTitleFormValue(event.target.value);
},
[setTodoTitleFormValue]
);
return (
<label>
タスク名:
<input
type="text"
value={todoTitleFormValue}
onChange={onChange}
name="title"
style={{ margin: 12 }}
/>
</label>
);
};
export default TitleForm;
同様に「検索文字列を入力するフィールド」コンポーネントも作成しておきます。
import { useCallback } from "react";
import { useRecoilValue, useSetRecoilState, SetterOrUpdater } from "recoil";
import { searchTextFormState } from "../atoms/SearchTextFormAtom";
const SearchForm: React.FC = () => {
const searchTextFormValue: string = useRecoilValue(searchTextFormState);
const setSearchTextFormValue: SetterOrUpdater<string> = useSetRecoilState(
searchTextFormState
);
const onChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setSearchTextFormValue(event.target.value);
},
[setSearchTextFormValue]
);
return (
<label>
検索:
<input
type="text"
value={searchTextFormValue}
onChange={onChange}
name="title"
style={{ margin: 12 }}
/>
</label>
);
};
export default SearchForm;
また「タスクの追加」を行うボタンコンポーネントも用意しておきましょう。
import { useCallback } from "react";
import { useRecoilValue, useSetRecoilState, SetterOrUpdater } from "recoil";
import { todoTitleFormState } from "../atoms/TodoTitleFormAtom";
import { todoListState } from "../atoms/TodoListAtom";
import Todo from "../types/Todo";
const AddButton: React.FC = () => {
const todoList: Todo[] = useRecoilValue(todoListState);
const todoTitleFormValue: string = useRecoilValue(todoTitleFormState);
const setTodoList: SetterOrUpdater<Todo[]> = useSetRecoilState(todoListState);
const setTitleFormValue: SetterOrUpdater<string> = useSetRecoilState(
todoTitleFormState
);
const onClick = useCallback(() => {
setTodoList([...todoList, { title: todoTitleFormValue }]);
// タスクを追加したら入力欄は空にする
setTitleFormValue("");
}, [todoList, todoTitleFormValue, setTodoList, setTitleFormValue]);
return <button onClick={onClick}>追加</button>;
};
export default AddButton;
Appの編集
これで必要なコンポーネントの作成が完了しました。
App.tsx
を以下のように書き換えて、Recoil
を使ったコンポーネント群を実際に使用してみましょう。
import "./App.css";
import TitleForm from "./component/TitleForm";
import AddButton from "./component/AddButton";
import TodoList from "./component/TodoList";
import SearchForm from "./component/SearchForm";
const App: React.FC = () => {
return (
<div style={{ margin: 12 }}>
<div>
<TitleForm />
<AddButton />
</div>
<SearchForm />
<TodoList />
</div>
);
};
export default App;
以下コマンドで問題なく起動すれば、冒頭で紹介したサンプルのようなアプリが立ち上がるかと思います。
yarn start
今回作成したもの
上記のソース一式をGitHub
の以下リポジトリにアップしてあります。
動作させながらソースを追いたい場合は参照ください。
まとめ
今回はRecoil
についてTypescript
を使った簡単なタスク管理アプリを作ることで紹介していきました。
普段はRedux
を使って開発していますが。Recoil
の方がシンプルかつ直感的な作りなので分かりやすいかなと思いました。
またHooks
のような使い勝手なのも今風のReact
アプリっぽいので、後発のライブラリの強みなのかなとも思います。
まだExperimental
扱いなので、今後使用感やサポートは大きく変わる可能性もありますが、個人的には今後伸びていくのではないかと思える有益ライブラリでした。
Discussion