3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-02-14

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

サンプル001■React + TypeScript: Recoil tutorial example 03

ディレクトリをコンポーネントと状態で分ける

簡単な作例ながら、それなりにモジュール数も増えました。そこで、コンポーネント(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するパスを修正しなければなりません。機械的な書き替えですので、コードを羅列します。動きが変わらず、エラーの出ないことを確かめてください。

src/index.tsx
// import App from './App';
import App from './components/App';
src/components/TodoItem.tsx
// import { todoListState } from './todoListState';
import { todoListState } from '../state/todoListState';
// import type { TodoItemType } from './todoListState';
import type { TodoItemType } from '../state/todoListState';
src/components/TodoItemCreator.tsx
// import { todoListState } from './todoListState';
import { todoListState } from '../state/todoListState';
src/components/TodoList.tsx
// import { filteredTodoListState } from './filteredTodoListState';
import { filteredTodoListState } from '../state/filteredTodoListState';
src/components/TodoListFilters.tsx
// import { todoListFilterState } from './todoListFilterState';
import { todoListFilterState } from '../state/todoListFilterState';
src/components/TodoListStats.tsx
// import { todoListStatsState } from './todoListStatsState';
import { todoListStatsState } from '../state/todoListStatsState';

フィルタ設定のロジックをコンポーネントから状態に移す

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

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 };
};
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)に移します。すると、コンポーネントは状態を直に参照することさえなくなるのです。

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 };
};
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を用いなければなりません。

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 };
};
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)を移します。

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 };
};
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の状態は参照するのみで、値を直に書き替えることはなくなったのです。

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,
	};
};
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アサーションを加えると、各プロパティも読み取り専用になるのです(「constアサーション『as const』(const assertion)」参照)

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

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

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

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

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で型アサーションしなければなりません。

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フックでメモ化しておく方がよいでしょう。

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作例をご参照ください。

3
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?