React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 01 ー atomを使った項目操作


* 「**[React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 02 ー selectorによるフィルタリングと集計](https://qiita.com/FumioNonaka/items/d1bf46eb5437813f10e1)**」
* 「**[React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 03 ー 構成とロジックを整える](https://qiita.com/FumioNonaka/items/dd0c988bae47ab82e012)**」
* 「**[React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 04 ー 状態を直接exportしないようにする](https://qiita.com/FumioNonaka/items/7368de737a8164bce3b1)**」


[Recoil](https://recoiljs.org/)はMeta(Facebook)が開発している、Reactの状態を管理するライブラリです。状態をひとまとめにするのではなく、ひとつひとつ細かく分けて扱います。「[React + TypeScript: Facebookの状態管理ライブラリRecoilを使ってみる](https://qiita.com/FumioNonaka/items/b254db610d6e8cc17026)」では、その大まかな仕組みを、簡単なコード例でご説明しました。

本シリーズのお題は、Recoil公式「[Basic Tutorial](https://recoiljs.org/docs/basic-tutorial/intro)」の作例とされているTodoリストです。ただし、コードはモジュール分けしたうえで、TypeScriptによる型定義も加えました。この第1回が扱うのは、`atom`を用いたリスト項目の追加・編集・削除の操作です(サンプル001)。

#### サンプル001■React + TypeScript: Recoil tutorial example 01
https://codesandbox.io/embed/react-typescript-recoil-tutorial-example-01-new-vhdcg2?fontsize=14&hidenavigation=1&theme=dark


# ルートのコンポーネントを定める
Reactアプリケーションのひな形は、Create React Appでつくります。つくり方ついては「[Create React AppでTypeScriptが加わったひな形アプリケーションをつくる](http://www.fumiononaka.com/Business/html5/FN2006001.html#sec01)」をお読みください。

状態を共有したいコンポーネントツリーのルートは、[`<RecoilRoot>`](https://recoiljs.org/docs/api-reference/core/RecoilRoot)で包まなければなりません(「[コンポーネントツリーを\<RecoilRoot\>でつつむ](https://qiita.com/FumioNonaka/items/b254db610d6e8cc17026#%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%83%84%E3%83%AA%E3%83%BC%E3%82%92recoilroot%E3%81%A7%E3%81%A4%E3%81%A4%E3%82%80)」参照)。これで、ツリー内のすべての子孫コンポーネントから、同一の状態が使えるようになります。親コンポーネントはあとで定める`TodoList`(コード003)です。

#### コード001■Todoリストのアプリケーションコンポーネント
```react:src/App.tsx
import { RecoilRoot } from 'recoil';
import { TodoList } from './TodoList';

export default function App() {
	return (
		<RecoilRoot>
			<TodoList />
		</RecoilRoot>
	);
}
```


# `atom`にTodoリストの状態を定める
`atom`はアプリケーションの状態を示す信頼すべきソース(source of truth)です(「[信頼できる唯一の情報源](https://ja.wikipedia.org/wiki/%E4%BF%A1%E9%A0%BC%E3%81%A7%E3%81%8D%E3%82%8B%E5%94%AF%E4%B8%80%E3%81%AE%E6%83%85%E5%A0%B1%E6%BA%90)」参照)。`todoListState`には、関数[`atom()`](https://recoiljs.org/docs/api-reference/core/atom)でTodo項目(`TodoItemType`型)のリスト(配列)データ(状態)を定めます。引数のオプションオブジェクトに与える`key`は状態の一意の識別子(`todoListState`)、`default`(`TodoItemType[]`型)が状態の初期値(`[]`)です。

#### コード002■Todo項目のリストデータを定める
```react:src/todoListState.ts
import { atom } from 'recoil';

export type TodoItemType = {
	id: number;
	text: string;
	isComplete: boolean;
};
export const todoListState = atom<TodoItemType[]>({
	key: 'todoListState',
	default: [],
});
```


# 状態を読み込む ー `useRecoilValue`フック
[`useRecoilValue()`](https://recoiljs.org/docs/api-reference/core/useRecoilValue/)は、引数の`atom`から値を読み取るフックです。前出「React + TypeScript: Facebookの状態管理ライブラリRecoilを使ってみる」(「[atomで状態を定める](https://qiita.com/FumioNonaka/items/b254db610d6e8cc17026#atom%E3%81%A7%E7%8A%B6%E6%85%8B%E3%82%92%E5%AE%9A%E3%82%81%E3%82%8B)」)で用いた`useRecoilState()`と異なり、設定の関数は得られません(つまり、読み取り専用)。

`TodoList`コンポーネントは、取り出したTodoリストの配列(`todoList`)から、このあと定める項目のコンポーネント`TodoItem`に要素(`todoItem`)のデータを順に与えてリスト表示します。

#### コード003■Todoリストの親コンポーネント
```react:src/TodoList.tsx
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()`](https://recoiljs.org/docs/api-reference/core/useSetRecoilState)には`atom`に状態を渡します。戻り値(`setTodoList`)が状態設定関数です。

状態設定関数`setTodoList`の呼び出しは、引数の定めに[関数型の更新](https://qiita.com/FumioNonaka/items/73cb59fb3cabce4106da#:~:text=%E3%82%B3%E3%83%BC%E3%83%AB%E3%83%90%E3%83%83%E3%82%AF%E3%81%AF%E3%80%8C-,%E6%9B%B4%E6%96%B0%E9%96%A2%E6%95%B0,-%E3%80%8D%E3%81%A8%E5%91%BC%E3%81%B0)を用いました。渡すのは、引数(`oldTodoList`)として更新前の値を受け取り、新たな値を返す関数です。現行のTodoリスト(`todoListState`)に新たな項目が加えられます。

#### コード004■Todoリスト項目をデータに加えるコンポーネント
```react:src/TodoItemCreator.tsx
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()`](https://recoiljs.org/docs/api-reference/core/useRecoilState/)フックです。構文は[`useState`](https://ja.react.dev/reference/react/useState)と同じで、戻り値の配列の第1要素(`todoList`)が状態変数、第2要素(`setTodoList`)は設定関数になります。違うのは、Recoilの状態はコンポーネントツリーの中で共有されるということです。

Todo項目のデータはコンポーネントが引数オブジェクト(`item`)で受け取り、編集(`editItemText`)やチェックのオン/オフ(`toggleItemCompletion`)、削除(`deleteItem`)の関数をつぎのコード005のように定めました。実際の動きは、CodeSandboxに公開した前掲サンプル001でお確かめください。

#### コード005■項目の表示とチェック・編集・削除を行うコンポーネント
```react:src/TodoItem.tsx
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>
	);
};
```

* 「**[React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 02 ー selectorによるフィルタリングと集計](https://qiita.com/FumioNonaka/items/d1bf46eb5437813f10e1)**」
* 「**[React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 03 ー 構成とロジックを整える](https://qiita.com/FumioNonaka/items/dd0c988bae47ab82e012)**」
* 「**[React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 04 ー 状態を直接exportしないようにする](https://qiita.com/FumioNonaka/items/7368de737a8164bce3b1)**」