- 「React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 02 ー selectorによるフィルタリングと集計」
- 「React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 03 ー 構成とロジックを整える」
- 「React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 04 ー 状態を直接exportしないようにする」
RecoilはMeta(Facebook)が開発している、Reactの状態を管理するライブラリです。状態をひとまとめにするのではなく、ひとつひとつ細かく分けて扱います。「React + TypeScript: Facebookの状態管理ライブラリRecoilを使ってみる」では、その大まかな仕組みを、簡単なコード例でご説明しました。
本シリーズのお題は、Recoil公式「Basic Tutorial」の作例とされているTodoリストです。ただし、コードはモジュール分けしたうえで、TypeScriptによる型定義も加えました。この第1回が扱うのは、atom
を用いたリスト項目の追加・編集・削除の操作です(サンプル001)。
サンプル001■React + TypeScript: Recoil tutorial example 01
ルートのコンポーネントを定める
Reactアプリケーションのひな形は、Create React Appでつくります。つくり方ついては「Create React AppでTypeScriptが加わったひな形アプリケーションをつくる」をお読みください。
状態を共有したいコンポーネントツリーのルートは、<RecoilRoot>
で包まなければなりません(「コンポーネントツリーを<RecoilRoot>でつつむ」参照)。これで、ツリー内のすべての子孫コンポーネントから、同一の状態が使えるようになります。親コンポーネントはあとで定めるTodoList
(コード003)です。
コード001■Todoリストのアプリケーションコンポーネント
import { RecoilRoot } from 'recoil';
import { TodoList } from './TodoList';
export default function App() {
return (
<RecoilRoot>
<TodoList />
</RecoilRoot>
);
}
atom
にTodoリストの状態を定める
atom
はアプリケーションの状態を示す信頼すべきソース(source of truth)です(「信頼できる唯一の情報源」参照)。todoListState
には、関数atom()
でTodo項目(TodoItemType
型)のリスト(配列)データ(状態)を定めます。引数のオプションオブジェクトに与えるkey
は状態の一意の識別子(todoListState
)、default
(TodoItemType[]
型)が状態の初期値([]
)です。
コード002■Todo項目のリストデータを定める
import { atom } from 'recoil';
export type TodoItemType = {
id: number;
text: string;
isComplete: boolean;
};
export const todoListState = atom<TodoItemType[]>({
key: 'todoListState',
default: [],
});
状態を読み込む ー useRecoilValue
フック
useRecoilValue()
は、引数のatom
から値を読み取るフックです。前出「React + TypeScript: Facebookの状態管理ライブラリRecoilを使ってみる」(「atomで状態を定める」)で用いたuseRecoilState()
と異なり、設定の関数は得られません(つまり、読み取り専用)。
TodoList
コンポーネントは、取り出したTodoリストの配列(todoList
)から、このあと定める項目のコンポーネントTodoItem
に要素(todoItem
)のデータを順に与えてリスト表示します。
コード003■Todoリストの親コンポーネント
import type { FC } from 'react';
import { useRecoilValue } from 'recoil';
import { TodoItem } from './TodoItem';
import { TodoItemCreator } from './TodoItemCreator';
import { todoListState } from './todoListState';
export const TodoList: FC = () => {
const todoList = useRecoilValue(todoListState);
return (
<>
<TodoItemCreator />
{todoList.map((todoItem) => (
<TodoItem key={todoItem.id} item={todoItem} />
))}
</>
);
};
状態の値を設定する ー useSetRecoilState
フック
TodoItemCreator
は、<input type="text">
要素に入力されたテキストをTodo項目としてtodoListState
の状態に加えるコンポーネントです(コード004)。状態設定に用いるフックuseSetRecoilState()
にはatom
に状態を渡します。戻り値(setTodoList
)が状態設定関数です。
状態設定関数setTodoList
の呼び出しは、引数の定めに関数型の更新を用いました。渡すのは、引数(oldTodoList
)として更新前の値を受け取り、新たな値を返す関数です。現行のTodoリスト(todoListState
)に新たな項目が加えられます。
コード004■Todoリスト項目をデータに加えるコンポーネント
import { useCallback, useState } from 'react';
import type { ChangeEventHandler, FC } from 'react';
import { useSetRecoilState } from 'recoil';
import { todoListState } from './todoListState';
// utility for creating unique Id
let id = 0;
const getId = () => {
return id++;
}
export const TodoItemCreator: FC = () => {
const [inputValue, setInputValue] = useState('');
const setTodoList = useSetRecoilState(todoListState);
const addItem = useCallback(() => {
setTodoList((oldTodoList) => [
...oldTodoList,
{
id: getId(),
text: inputValue,
isComplete: false,
},
]);
setInputValue('');
}, [inputValue, setTodoList]);
const onChange: ChangeEventHandler<HTMLInputElement> = useCallback(
({ target: { value } }) => {
setInputValue(value);
},
[]
);
return (
<div>
<input type="text" value={inputValue} onChange={onChange} />
<button onClick={addItem}>Add</button>
</div>
);
};
状態の値を読み書きする ー useRecoilState
フック
Todoリストの状態(atom
)のデータから取り出した、ひとつひとつの項目を表示するのがTodoItem
コンポーネントです。処理済みかどうか示すチェックボックスと、項目の削除ボタンを備え、項目テキストの編集(追加)も行います。つまり、状態の値を読み書きできなければなりません。
atom
の状態の読み書きに用いるのがuseRecoilState()
フックです。構文はuseState
と同じで、戻り値の配列の第1要素(todoList
)が状態変数、第2要素(setTodoList
)は設定関数になります。違うのは、Recoilの状態はコンポーネントツリーの中で共有されるということです。
Todo項目のデータはコンポーネントが引数オブジェクト(item
)で受け取り、編集(editItemText
)やチェックのオン/オフ(toggleItemCompletion
)、削除(deleteItem
)の関数をつぎのコード005のように定めました。実際の動きは、CodeSandboxに公開した前掲サンプル001でお確かめください。
コード005■項目の表示とチェック・編集・削除を行うコンポーネント
import { useCallback } from 'react';
import type { ChangeEventHandler, FC } from 'react';
import { useRecoilState } from 'recoil';
import { todoListState } from './todoListState';
import type { TodoItemType } from './todoListState';
type Props = {
item: TodoItemType;
};
const replaceItemAtIndex = (
arr: TodoItemType[],
index: number,
newValue: TodoItemType
) => {
return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)];
};
const removeItemAtIndex = (arr: TodoItemType[], index: number) => {
return [...arr.slice(0, index), ...arr.slice(index + 1)];
};
export const TodoItem: FC<Props> = ({ item }) => {
const [todoList, setTodoList] = useRecoilState(todoListState);
const index = todoList.findIndex((listItem) => listItem === item);
const editItemText: ChangeEventHandler<HTMLInputElement> = useCallback(
({ target: { value } }) => {
const newList = replaceItemAtIndex(todoList, index, {
...item,
text: value,
});
setTodoList(newList);
},
[index, item, setTodoList, todoList]
);
const toggleItemCompletion = useCallback(() => {
const newList = replaceItemAtIndex(todoList, index, {
...item,
isComplete: !item.isComplete,
});
setTodoList(newList);
}, [index, item, setTodoList, todoList]);
const deleteItem = useCallback(() => {
const newList = removeItemAtIndex(todoList, index);
setTodoList(newList);
}, [index, setTodoList, todoList]);
return (
<div>
<input type="text" value={item.text} onChange={editItemText} />
<input
type="checkbox"
checked={item.isComplete}
onChange={toggleItemCompletion}
/>
<button onClick={deleteItem}>X</button>
</div>
);
};