7
11

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

Last updated at Posted at 2022-02-03

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リストのアプリケーションコンポーネント

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)です(「信頼できる唯一の情報源」参照)。todoListStateには、関数atom()でTodo項目(TodoItemType型)のリスト(配列)データ(状態)を定めます。引数のオプションオブジェクトに与えるkeyは状態の一意の識別子(todoListState)、default(TodoItemType[]型)が状態の初期値([])です。

コード002■Todo項目のリストデータを定める

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()は、引数のatomから値を読み取るフックです。前出「React + TypeScript: Facebookの状態管理ライブラリRecoilを使ってみる」(「atomで状態を定める」)で用いたuseRecoilState()と異なり、設定の関数は得られません(つまり、読み取り専用)。

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

コード003■Todoリストの親コンポーネント

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()にはatomに状態を渡します。戻り値(setTodoList)が状態設定関数です。

状態設定関数setTodoListの呼び出しは、引数の定めに関数型の更新を用いました。渡すのは、引数(oldTodoList)として更新前の値を受け取り、新たな値を返す関数です。現行のTodoリスト(todoListState)に新たな項目が加えられます。

コード004■Todoリスト項目をデータに加えるコンポーネント

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()フックです。構文はuseStateと同じで、戻り値の配列の第1要素(todoList)が状態変数、第2要素(setTodoList)は設定関数になります。違うのは、Recoilの状態はコンポーネントツリーの中で共有されるということです。

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

コード005■項目の表示とチェック・編集・削除を行うコンポーネント

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>
	);
};
7
11
0

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
7
11