React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 03 ー 構成とロジックを整える


* 「**[React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 01 ー atomを使った項目操作](https://qiita.com/FumioNonaka/items/3ed6b562a20f6d4abb2b)**」
* 「**[React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 02 ー selectorによるフィルタリングと集計](https://qiita.com/FumioNonaka/items/d1bf46eb5437813f10e1)**」
* 「**[React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 04 ー 状態を直接exportしないようにする](https://qiita.com/FumioNonaka/items/7368de737a8164bce3b1)**」


Recoil公式チュートリアルの解説は前回で終えました。この第3回はスピンオフです。とくに動きは変えることなく、モジュールの構成とロジックを整理します。せっかくモジュール分けもしましたので、もう少し実践に近づけようという試みです。

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

# ディレクトリをコンポーネントと状態で分ける
簡単な作例ながら、それなりにモジュール数も増えました。そこで、コンポーネント(`components/`)と状態(`state/`)を、つぎのようにディレクトリ分けすることにします。

* `src/components/`
    * `App.tsx`
    * `TodoItem.tsx`
    * `TodoItemCreator.tsx`
    * `TodoList.tsx`
    * `TodoListFilters.tsx`
    * `TodoListStats.tsx`
* `src/state/`
    * `filteredTodoListState.ts`
    * `todoListFilterState.ts`
    * `todoListState.ts`
    * `todoListStatsState.ts`

すると、各モジュールが`import`するパスを修正しなければなりません。機械的な書き替えですので、コードを羅列します。動きが変わらず、エラーの出ないことを確かめてください。

```react:src/index.tsx
// import App from './App';
import App from './components/App';
```

```react:src/components/TodoItem.tsx
// import { todoListState } from './todoListState';
import { todoListState } from '../state/todoListState';
// import type { TodoItemType } from './todoListState';
import type { TodoItemType } from '../state/todoListState';
```

```react:src/components/TodoItemCreator.tsx
// import { todoListState } from './todoListState';
import { todoListState } from '../state/todoListState';
```

```react:src/components/TodoList.tsx
// import { filteredTodoListState } from './filteredTodoListState';
import { filteredTodoListState } from '../state/filteredTodoListState';
```

```react:src/components/TodoListFilters.tsx
// import { todoListFilterState } from './todoListFilterState';
import { todoListFilterState } from '../state/todoListFilterState';
```

```react:src/components/TodoListStats.tsx
// import { todoListStatsState } from './todoListStatsState';
import { todoListStatsState } from '../state/todoListStatsState';
```


# フィルタ設定のロジックをコンポーネントから状態に移す
フィルタの設定はコンポーネント(`TodoListFilters`)から行うのでなく、状態(`todoListFilterState`)にカスタムフック(`useFilter`)として移します。コードはむしろ増えるものの、フックを介すことによりコンポーネントが直に状態を変えることはなくなるのです。そのため、コンポーネントが用いるRecoilのフックも、値を参照するだけの`useRecoilValue`に差し替えました。

```react:src/state/todoListFilterState.ts
import { useCallback } from 'react';
// import { atom } from 'recoil';
import { atom, useSetRecoilState } from 'recoil';

export const useFilter = () => {
	const setFilter = useSetRecoilState(todoListFilterState);
	const setListFilter = useCallback(
		(filter: string) => {
			setFilter(filter);
		},
		[setFilter]
	);
	return { setListFilter };
};
```

```react:src/components/TodoListFilters.tsx
// import { useRecoilState } from 'recoil';
import { useRecoilValue } from 'recoil';
// import { todoListFilterState } from '../state/todoListFilterState';
import { todoListFilterState, useFilter } from '../state/todoListFilterState';

export const TodoListFilters: FC = () => {
	// const [filter, setFilter] = useRecoilState(todoListFilterState);
	const filter = useRecoilValue(todoListFilterState);
	const { setListFilter } = useFilter();
	const updateFilter: ChangeEventHandler<HTMLSelectElement> = useCallback(
		({ target: { value } }) => {
			// setFilter(value);
			setListFilter(value);
		},
		// [setFilter]
		[setListFilter]
	);

};
```


# Todoリストへの項目データ追加をコンポーネントから状態に移す
Todoリストに項目を追加するコンポーネント(`TodoItemCreator`)のロジックも、同じように状態(`todoListState`)のフック(`useTodoList`)に移します。すると、コンポーネントは状態を直に参照することさえなくなるのです。

```react:src/state/todoListState.ts
import { useCallback } from 'react';
// import { atom } from 'recoil';
import { atom, useSetRecoilState } from 'recoil';

let id = 0;
const getId = () => {
	return id++;
}
export const useTodoList = () => {
	const setTodoList = useSetRecoilState(todoListState);
	const addListItem = useCallback(
		(text: string) => {
			setTodoList((oldTodoList) => [
				...oldTodoList,
				{
					id: getId(),
					text,
					isComplete: false,
				},
			]);
		},
		[setTodoList]
	);
	return { addListItem };
};
```

```react:src/components/TodoItemCreator.tsx
// import { useSetRecoilState } from 'recoil';
// import { todoListState } from '../state/todoListState';
import { useTodoList } from '../state/todoListState';

/* let id = 0;
const getId = () => {
	return id++;
} */

export const TodoItemCreator: FC = () => {

	// const setTodoList = useSetRecoilState(todoListState);
	const { addListItem } = useTodoList();
	const addItem = useCallback(() => {
		/* setTodoList((oldTodoList) => [
			...oldTodoList,
			{
				id: getId(),
				text: inputValue,
				isComplete: false,
			},
		]); */
		addListItem(inputValue);

		// }, [inputValue, setTodoList]);
	}, [addListItem, inputValue]);

};
```


# Todoリストの項目編集・チェック・削除の操作をコンポーネントでなく状態に備える
Todoリストの各項目を編集・チェック・削除する`TodoItem`は、もっともロジックの多いコンポーネントです。操作を順に状態(`todoListState`)に移しましょう。

## Todoリストの項目をフックで編集する
まず、Todoリスト項目の編集です。コンポーネント(`TodoItem`)から状態(`todoListState`)のフック(`useTodoList`)に移します。カスタムフックから状態の設定だけでなく参照もできるように、Recoilの`useRecoilState`を用いなければなりません。

```react:src/state/todoListState.ts
// import { atom, useSetRecoilState } from 'recoil';
import { atom, useRecoilState } from 'recoil';

export type TodoItemType = {
	id: number;
	text: string;
	isComplete: boolean;
};

const replaceItemAtIndex = (
	arr: TodoItemType[],
	index: number,
	newValue: TodoItemType
) => {
	return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)];
}
export const useTodoList = () => {
	// const setTodoList = useSetRecoilState(todoListState);
	const [todoList, setTodoList] = useRecoilState(todoListState);

	const editItemTextAtIndex = useCallback(
		(index: number, item: TodoItemType, text: string) => {
			const newList = replaceItemAtIndex(todoList, index, {
				...item,
				text,
			});
			setTodoList(newList);
		},
		[setTodoList, todoList]
	);
	// return { addListItem };
	return { addListItem, editItemTextAtIndex };
};
```

```react:src/components/TodoItem.tsx
// import { todoListState } from '../state/todoListState';
import { todoListState, useTodoList } from '../state/todoListState';

export const TodoItem: FC<Props> = ({ item }) => {

	const { editItemTextAtIndex } = useTodoList();

	const editItemText: ChangeEventHandler<HTMLInputElement> = useCallback(
		({ target: { value } }) => {
			/* const newList = replaceItemAtIndex(todoList, index, {
				...item,
				text: value,
			});
			setTodoList(newList); */
			editItemTextAtIndex(index, item, value);
		},
		// [index, item, setTodoList, todoList]
		[editItemTextAtIndex, index, item]
	);

};
```

## Todo項目のチェックを切り替える
つぎは、Todo項目のチェックの切り替えです。コンポーネント(`TodoItem`)から状態(`todoListState`)のフック(`useTodoList`)に関数(`toggleItemCompletionAtIndex`)を移します。

```react:src/state/todoListState.ts
export const useTodoList = () => {

	const toggleItemCompletionAtIndex = useCallback(
		(index: number, item: TodoItemType) => {
			const newList = replaceItemAtIndex(todoList, index, {
				...item,
				isComplete: !item.isComplete,
			});
			setTodoList(newList);
		},
		[setTodoList, todoList]
	);
	// return { addListItem, editItemTextAtIndex };
	return { addListItem, editItemTextAtIndex, toggleItemCompletionAtIndex };
};
```

```react:src/components/TodoItem.tsx
/* const replaceItemAtIndex = (

}; */

export const TodoItem: FC<Props> = ({ item }) => {

	// const { editItemTextAtIndex } = useTodoList();
	const { editItemTextAtIndex, toggleItemCompletionAtIndex } = useTodoList();

	const toggleItemCompletion = useCallback(() => {
		/* const newList = replaceItemAtIndex(todoList, index, {
			...item,
			isComplete: !item.isComplete,
		});
		setTodoList(newList); */
		toggleItemCompletionAtIndex(index, item);
		// }, [index, item, setTodoList, todoList]);
	}, [index, item, toggleItemCompletionAtIndex]);

};
```


## Todoリストから項目を削除する
最後は、Todoリストからの項目の削除です。コンポーネント(`TodoItem`)のロジックを、状態(`todoListState`)のフック(`useTodoList`)に移します。もはやコンポーネントから状態を書き替える必要がありません。用いるRecoilのフックは、読み取り専用の`useRecoilValue`に改めました。これで、すべてのコンポーネントからRecoilの状態は参照するのみで、値を直に書き替えることはなくなったのです。

```react:src/state/todoListState.ts
const removeItemAtIndex = (arr: TodoItemType[], index: number) => {
	return [...arr.slice(0, index), ...arr.slice(index + 1)];
}
export const useTodoList = () => {

	const deleteItemAtIndex = useCallback(
		(index: number) => {
			const newList = removeItemAtIndex(todoList, index);
			setTodoList(newList);
		},
		[setTodoList, todoList]
	);
	// return { addListItem, editItemTextAtIndex, toggleItemCompletionAtIndex };
	return {
		addListItem,
		deleteItemAtIndex,
		editItemTextAtIndex,
		toggleItemCompletionAtIndex,
	};
};
```

```react:src/components/TodoItem.tsx
// import { useRecoilState } from 'recoil';
import { useRecoilValue } from 'recoil';

/* const removeItemAtIndex = (arr: TodoItemType[], index: number) => {

} */
export const TodoItem: FC<Props> = ({ item }) => {
	// const [todoList, setTodoList] = useRecoilState(todoListState);
	const todoList = useRecoilValue(todoListState);
	// const { editItemTextAtIndex, toggleItemCompletionAtIndex } = useTodoList();
	const {
		deleteItemAtIndex,
		editItemTextAtIndex,
		toggleItemCompletionAtIndex,
	} = useTodoList();

	const deleteItem = useCallback(() => {
		/* const newList = removeItemAtIndex(todoList, index);
		setTodoList(newList); */
		deleteItemAtIndex(index);
		// }, [index, setTodoList, todoList]);
	}, [deleteItemAtIndex, index]);

};
```


# フィルタの値の定数化と型定義
フィルタに設定する値は、3つの文字列だけです。だとすれば、定数にしてしまえると、管理しやすくスペルミスも防げます。ところが、標準JavaScriptで`const`宣言したオブジェクトは、上書きができないだけです。それぞれのプロパティ値は書き替えられてしまいます。そういうとき、オブジェクトに[`const`アサーション](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions)を加えると、各プロパティも読み取り専用になるのです(「[constアサーション『as const』(const assertion)](https://typescriptbook.jp/reference/values-types-variables/const-assertion)」参照)

```react:src/state/todoListFilterState.ts
const FilterValue = {
	SHOW_ALL: 'Show All',
	SHOW_COMPLETED: 'Show Completed',
	SHOW_UNCOMPLETED: 'Show Uncompleted'
} as const;
```

さらに、3つの値しかとれない型が定められるならより安全になります。TypeScriptの演算子`keyof`と`typeof`を組み合わせれば、そのようなユニオン型がつくれるのです(「[オブジェクトからキーの型を生成する](https://typescriptbook.jp/tips/generates-type-from-object-key)」および「[TypeScriptの『typeof X[keyof typeof X]』の意味を順を追って理解する](https://nishinatoshiharu.com/keyof-typeof/)」参照)。

```react:src/state/todoListFilterState.ts
type FilterType = typeof FilterValue[keyof typeof FilterValue];
// つぎのユニオン型になる
// type FilterType = 'Show All' | 'Show Completed' | 'Show Uncompleted'
```

改めて、フィルタの状態のモジュール(`src/state/todoListFilterState.ts`)は、つぎのように書き直します。

```react:src/state/todoListFilterState.ts
export const FilterValue = {
	SHOW_ALL: 'Show All',
	SHOW_COMPLETED: 'Show Completed',
	SHOW_UNCOMPLETED: 'Show Uncompleted'
} as const;
export type FilterType = typeof FilterValue[keyof typeof FilterValue];
// export const todoListFilterState = atom<string>({
export const todoListFilterState = atom<FilterType>({

	// default: 'Show All',
	default: FilterValue.SHOW_ALL,
});
export const useFilter = () => {

	const setListFilter = useCallback(
		// (filter: string) => {
		(filter: FilterType) => {

		},

	);

};
```

フィルタを選択するコンポーネントも、`import`した型(`FilterType`)と値(`FilterValue`)を使って書き改めます。フィルタの状態に設定する(`setListFilter`の引数)値の型は、これまでの`string`では合わなくなりましたので、`FilterType`で型アサーションしなければなりません。

```react:src/components/TodoListFilters.tsx
// import { todoListFilterState, useFilter } from '../state/todoListFilterState';
import {
	FilterValue,
	todoListFilterState,
	useFilter,
} from '../state/todoListFilterState';
import type { FilterType } from '../state/todoListFilterState';

export const TodoListFilters: FC = () => {

	const updateFilter: ChangeEventHandler<HTMLSelectElement> = useCallback(
		({ target: { value } }) => {
			// setListFilter(value);
			setListFilter(value as FilterType);
		},
		[setListFilter]
	);
	return (
		<>

			<select value={filter} onChange={updateFilter}>
				{/* <option value="Show All">All</option> */}
				<option value={FilterValue.SHOW_ALL}>All</option>
				{/* <option value="Show Completed">Completed</option> */}
				<option value={FilterValue.SHOW_COMPLETED}>Completed</option>
				{/* <option value="Show Uncompleted">Uncompleted</option> */}
				<option value={FilterValue.SHOW_UNCOMPLETED}>Uncompleted</option>
			</select>
		</>
	);
};
```

もうひとつだけ、Todoリスト項目がみずからを特定するために用いるキーの`index`です。これは、[`useMemo`](https://qiita.com/FumioNonaka/items/86f6a91b98844beaff0d)フックでメモ化しておく方がよいでしょう。

```react:src/components/TodoItem.tsx
// import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';

export const TodoItem: FC<Props> = ({ item }) => {

	// const index = todoList.findIndex((listItem) => listItem === item);
	const index = useMemo(
		() => todoList.findIndex((listItem) => listItem === item),
		[item, todoList]
	);

};
```

フィルタの値は3つにかぎられ、その型も定められました。他のモジュールからの扱いがしやすく、安全になったでしょう。でき上がったTodoリストアプリケーションの各モジュールのコードは、冒頭に掲げたサンプル001のCodeSandbox作例をご参照ください。


* 「**[React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 01 ー atomを使った項目操作」](https://qiita.com/FumioNonaka/items/3ed6b562a20f6d4abb2b)**」
* 「**[React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 02 ー selectorによるフィルタリングと集計](https://qiita.com/FumioNonaka/items/d1bf46eb5437813f10e1)**」
* 「**[React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 04 ー 状態を直接exportしないようにする](https://qiita.com/FumioNonaka/items/7368de737a8164bce3b1)**」