- 「React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 01 ー atomを使った項目操作」
- 「React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 02 ー selectorによるフィルタリングと集計」
- 「React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 04 ー 状態を直接exportしないようにする」
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
するパスを修正しなければなりません。機械的な書き替えですので、コードを羅列します。動きが変わらず、エラーの出ないことを確かめてください。
// import App from './App';
import App from './components/App';
// import { todoListState } from './todoListState';
import { todoListState } from '../state/todoListState';
// import type { TodoItemType } from './todoListState';
import type { TodoItemType } from '../state/todoListState';
// import { todoListState } from './todoListState';
import { todoListState } from '../state/todoListState';
// import { filteredTodoListState } from './filteredTodoListState';
import { filteredTodoListState } from '../state/filteredTodoListState';
// import { todoListFilterState } from './todoListFilterState';
import { todoListFilterState } from '../state/todoListFilterState';
// import { todoListStatsState } from './todoListStatsState';
import { todoListStatsState } from '../state/todoListStatsState';
フィルタ設定のロジックをコンポーネントから状態に移す
フィルタの設定はコンポーネント(TodoListFilters
)から行うのでなく、状態(todoListFilterState
)にカスタムフック(useFilter
)として移します。コードはむしろ増えるものの、フックを介すことによりコンポーネントが直に状態を変えることはなくなるのです。そのため、コンポーネントが用いるRecoilのフックも、値を参照するだけのuseRecoilValue
に差し替えました。
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 };
};
// 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
)に移します。すると、コンポーネントは状態を直に参照することさえなくなるのです。
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 };
};
// 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
を用いなければなりません。
// 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 };
};
// 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
)を移します。
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 };
};
/* 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の状態は参照するのみで、値を直に書き替えることはなくなったのです。
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,
};
};
// 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)」参照)
const FilterValue = {
SHOW_ALL: 'Show All',
SHOW_COMPLETED: 'Show Completed',
SHOW_UNCOMPLETED: 'Show Uncompleted'
} as const;
さらに、3つの値しかとれない型が定められるならより安全になります。TypeScriptの演算子keyof
とtypeof
を組み合わせれば、そのようなユニオン型がつくれるのです(「オブジェクトからキーの型を生成する」および「TypeScriptの『typeof X[keyof typeof X]』の意味を順を追って理解する」参照)。
type FilterType = typeof FilterValue[keyof typeof FilterValue];
// つぎのユニオン型になる
// type FilterType = 'Show All' | 'Show Completed' | 'Show Uncompleted'
改めて、フィルタの状態のモジュール(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
で型アサーションしなければなりません。
// 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
フックでメモ化しておく方がよいでしょう。
// 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作例をご参照ください。