Recoilで状態管理やってみた
こんにちは!
本記事は、TECH PLAY女子部 Advent Calendar 2022の23日目の記事です!
Reactにおける状態管理に関係するライブラリには、Reduxなどいくつかありますよね。
今回は、Recoilを扱いたいと思います。
最近、業務でRecoilを少し触る機会がありました。
折角なので、業務で扱ったもの以外にも基本的な部分は押さえておきたいな〜と思って、勉強してみました。
本記事では、Recoilについて、簡単なToDoリストを作りながら、ご紹介していこうと思います!
Recoilとは
Recoilとは、Meta(Facebook)が開発を行なっている、Reactの状態管理ライブラリです。
公式のGitHubリポジトリを確認すると、experimental
(実験的)となっており、正式リリースはまだ行われておりません。
Recoilの導入
1. インストール
まずは、インストールを行います。
使用しているパッケージマネージャがnpm
の場合は、以下のコマンドでインストールします。
npm install recoil
yarn
を使用している場合は、以下のコマンドです。
yarn add recoil
2. Recoilを使用できるようにする
Recoilは、インストールしただけでは使えません。
ルートコンポーネント(App.tsx
など)に、以下のように<RecoilRoot>
要素を記述します。
import { RecoilRoot } from 'recoil';
const App = () => {
return (
<RecoilRoot>
<div>
<h1>Recoilサンプル</h1>
</div>
</RecoilRoot>
);
};
export default App;
これで、Recoilを使う準備は完了です!
Recoilの基本の使い方
ここからは、早速、Recoilの基本的な使い方について紹介していきます。
1. atomとselector
まずは、atomとselectorについてです。
①atomとは
atom
は、一意のkeyとデフォルト値を持ち、stateを管理しています。
Reduxだと、基本的にはstateは1箇所にまとめて管理する必要がありますが、Recoilだと、それぞれ管理することができます。管理したいstateごとにatom
関数を使って作成するだけです。
公式ドキュメントにて、以下のような記載があります。
Atoms contain the source of truth for our application state.
出典)Recoil 公式ドキュメント Basic Tutorial Atoms
atom
は、加工されていない、正式なstateの値を管理しています。(なぜ、このような言い方をしているのかは次の項目で何となくわかるかも?)
例えば、以下のように使います。
import { atom } from 'recoil';
const sampleAtom = atom<string[]>({
key: 'sampleAtom',
default: [
'sample1',
'sample2',
'sample3'
]
});
export {
sampleAtom
};
上記の例では、文字列を要素とする配列をデフォルト値として持つatomを作成しています。
②selectorとは
selector
関数を使い、atom
や他のselector
の値を使って計算や加工を施した値を返したり、atom
を更新したりできます。atom
から派生した、派生stateと呼びます。(公式ではderived stateと記載されている。)
もう少し詳しく説明してみます。
get
プロパティは、atom
や他のselector
の値を利用して何らかの計算や加工を施した値を返します。こちらは、読み取り専用(RecoilValueReadOnly
)の値です。また、set
プロパティは、atom
へ書き込みを行い、更新することができます。get
プロパティは必須ですが、set
プロパティはオプションなので、不要であれば使わないで大丈夫です。
例えば、以下のように使います。
import { atom, selector } from 'recoil';
const sampleAtom = atom<string[]>({
key: 'sampleAtom',
default: [
'sample1',
'sample2',
'sample3'
]
});
// selector
const sampleSelector = selector<string[]>({
key: 'sampleSelector',
get: ({ get }) => {
return get(sampleAtom);
}
});
export {
sampleAtom,
sampleSelector
};
作成したselectorsampleSelector
に注目してください。
ここでは、state(atomsampleAtom
)の値を取得しています。
get(state)
get
関数に、state(atom
もしくはselector
)を渡すことで、指定したatom
もしくはselector
の値を取得することができます。
set
プロパティを利用する場合、例えば以下のような記述になります。
const sampleSelector = selector<string[]>({
key: 'sampleSelector',
get: ({ get }) => {
return get(sampleAtom);
},
set: ({ get, set }, newValue) => {
// 例えば、newValueとして「さんぷる」が入ってくるとする。
set(sampleAtom, [...get(sampleAtom), newValue]);
}
});
set
プロパティを利用してatom
への書き込みを行う場合、実行にはuseSetRecoilState
フックを利用します。(useSetRecoilState
自体の使い方については、後述します。)
このフックが返す関数を使ってこのselectorを実行する時に、引数として値を渡すと第2引数(上記コードで言うnewValue
にあたる)にその値が入ってくることになり、その入ってきた値を使ってstateの更新を行うことができます。
2. stateの取得と更新
ここからは、stateの取得・更新で使うフックをいくつかご紹介します。
使うatom
は先ほどと同じ、以下になります。
import { atom } from 'recoil';
const sampleAtom = atom<string[]>({
key: 'sampleAtom',
default: [
'sample1',
'sample2',
'sample3'
]
});
export {
sampleAtom
};
①useRecoilValue
useRecoilValue
は、stateを返すだけです。
stateが更新されるたびに、再描画されます。
useRecoilValue(state)
引数に、state(atom
もしくはselector
)を渡します。
例えば、以下のように使います。
import { useRecoilValue } from 'recoil';
import { sampleAtom, sampleSelector } from '../recoil/sampleState';
const Sample = () => {
// useRecoilValue
const sampleAtomVal = useRecoilValue<string[]>(sampleAtom);
return (
<div>
<ul>
{sampleAtomVal.map((sampleAtom: string, index: number) => (
<li key={index}>{sampleAtom}</li>
))}
</ul>
</div>
);
};
export default Sample;
useRecoilValue
の引数にatomsampleAtom
を渡しています。
画面では、以下の画像のように、sampleAtom
が持つstateの配列が展開されて表示されます。
②useSetRecoilState
useSetRecoilState
は、stateを更新するだけです。新しい値をセットするだけで、新しい値を返したりはしません。
useSetRecoilState(state)
引数に、state(atom
もしくはselector
)を渡します。
例えば、以下のように使います。
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { sampleAtom } from '../recoil/sampleState';
const Sample = () => {
const sampleAtomVal = useRecoilValue<string[]>(sampleAtom);
// useSetRecoilState
const setSampleAtomVal = useSetRecoilState<string[]>(sampleAtom);
// 新しい値をセットして、stateを更新
const changeSampleAtomVal = (): void => {
// 新しい値
const newItem: string = 'サンプル4';
// 型が文字列を要素とする配列なので、配列にする
const newArray : string[]= [newItem];
// atomに新しい値を入れた配列をセットして、stateを更新
setSampleAtomVal(newArray);
};
return (
<div>
<ul>
{sampleAtomVal.map((sampleAtom: string, index: number) => (
<li key={index}>{sampleAtom}</li>
))}
</ul>
{/* このボタンを押すと、stateを更新できる */}
<button onClick={changeSampleAtomVal}>チェンジ!</button>
</div>
);
};
export default Sample;
useSetRecoilState
の引数にatomsampleAtom
を渡しています。
「チェンジ!」ボタン押下前と後の画面では、以下の画像のように表示されます。
【ボタン押下前】
【ボタン押下後】
「チェンジ!」ボタン押下後には、stateの配列の要素は、サンプル4
のみになっています。
useSetRecoilState
では、stateの更新はできるようになりますが、値は返しません。そのため、stateの取得には、useRecoilValue
を使っています。
③useRecoilState
useRecoilState
は、stateの取得と更新の両方ができます。新しい値でstateを更新して、更新された値を取得することができます。
useRecoilState(state)
引数に、state(atom
もしくはselector
)を渡します。
例えば、以下のように使います。
import { useRecoilState } from 'recoil';
import { sampleAtom } from '../recoil/sampleState';
const Sample = () => {
// useRecoilState
const [sampleAtomVal, setSampleAtomVal] = useRecoilState(sampleAtom);
// 新しい値をセットして、stateを更新
const changeSampleAtomVal = (): void => {
// 新しい値
const newItem: string = 'new sample';
// 現在のstateの配列に、新しい値を追加
const newArray : string[]= [...sampleAtomVal, newItem];
// 新しい値が追加された配列をセットして、stateを更新
setSampleAtomVal(newArray);
};
return (
<div>
<ul>
{sampleAtomVal.map((sampleAtom: string, index: number) => (
<li key={index}>{sampleAtom}</li>
))}
</ul>
{/* このボタンを押すと、stateを更新できる */}
<button onClick={changeSampleAtomVal}>チェンジ!</button>
</div>
);
};
export default Sample;
useSetRecoilState
の引数にatomsampleAtom
を渡しています。
「チェンジ!」ボタン押下前と後の画面では、以下の画像のように表示されます。
【ボタン押下前】
【ボタン押下後】
「チェンジ!」ボタン押下後には、stateの配列の要素に、new sample
が追加されています。
useRecoilState
は、stateの更新だけでなく取得もできるので、ここではuseRecoilValue
は不要です。
Recoilを使ってToDoリストを作ってみる!
ここからは、上記でご紹介したことについて、ToDoリストを作りながら、もう少し細かく見ていきたいと思います。
Recoil公式ドキュメントにおいても、ToDoリストアプリを作成するチュートリアルが紹介されています。
こういうのを作ります。
※冒頭でお伝えしていたように、スタイリング関係については触れません。
1. atomを用意
まずは、stateを管理するatom
を用意するところから始めます。
import { atom } from 'recoil';
export type TaskAtomType = {
id: number;
title: string;
edit: boolean;
isCompleted: boolean;
};
export type AllTasksAtomType = TaskAtomType[];
// 全てのタスクを格納しておくatom
const allTasksAtom = atom<AllTasksAtomType>({
key: 'allTasksAtom',
default: []
});
全てのタスクを配列にして格納しておくためのatomallTasksAtom
を作成します。
それぞれのタスクについては、id
、タスク名
、編集可否の真偽値
、完了状況の真偽値
を持つオブジェクトになっています。
※タスクの編集については、今回は触れません。
2. タスクを追加する
それではまず、タスク追加処理をやっていきたいと思います。
フォームに入力されたタスクを追加していきます。
以下のようなselector
を用意します。
// タスク追加のselector
const taskAddSelector = selector<AllTasksAtomType>({
key: 'taskAddSelector',
get: ({ get }) => {
return get(allTasksAtom); // 読み取り専用
},
set: ({ get, set }, title: any) => {
const currentAtom: AllTasksAtomType = get(allTasksAtom); // 現在のstate(atom)を取得
const currentAtomLength: number = currentAtom.length; // 現在のstate(atom)の配列の長さを取得
let addTaskObj: TaskAtomType | undefined = undefined; // set関数実行時に渡す
if (currentAtomLength === 0) {
/*
現在のstate(atom)の配列に何も要素が入っていない場合、
追加するタスクのidは1とする
*/
addTaskObj = {
id: 1,
title: title,
edit: false,
isCompleted: false
};
} else {
/*
現在のstate(atom)の配列に1つ以上要素が入っている場合、
追加するタスクのidは現在の配列の長さ+1とする(最後尾に追加する)
*/
addTaskObj = {
id: currentAtomLength + 1,
title: title,
edit: false,
isCompleted: false
};
}
set(allTasksAtom, [...currentAtom, addTaskObj]);
}
});
ここでは、set
プロパティを利用しています。atom
を更新できます。
引数として渡されているtitle
には、入力された値が入ってきます。
if文の後にある、set
関数に注目してみます。
set(`更新したいstate`, `新しい値`);
第1引数には、更新したいstate(atom
)のallTasksAtom
を渡します。そして、第2引数には、更新後の新しい値(上記で言う、タスクが追加された状態の配列)を渡します。
これにより、指定したstateに新しい値が書き込まれ、stateが更新されます。
それでは、タスク追加フォーム側の処理もやっていきます。
次のように、AddForm.tsx
ファイルにAddForm
コンポーネントを作っていきます。
import { useForm } from 'react-hook-form';
import { useSetRecoilState } from 'recoil';
import { taskAddSelector } from '../recoil/recoilState';
import { AddTaskType } from '../types/addTasksType';
import { formContent, registerButton } from '../styles/addForm';
const AddForm = () => {
const { register, handleSubmit, reset } = useForm({
defaultValues: {
title: ''
}
});
const setNewAllTasks = useSetRecoilState<any>(taskAddSelector);
// タスクを追加
const addTask = (data: AddTaskType): void => {
const taskTitle: string = data.title;
// selectorの実行
setNewAllTasks(taskTitle);
reset(); // フォームを空にする
}
return (
<div>
<form>
<input {...register('title')} placeholder='ここにタスク名を入力' />
<button type='submit' onClick={handleSubmit(addTask)}>登録</button>
</form>
</div>
);
};
export default AddForm;
stateの更新を行うため、以下のようにuseSetRecoilState
を利用しています。
const setNewAllTasks = useSetRecoilState<any>(taskAddSelector);
一旦、型については置いておいて。(selector使うときの型指定の最適解が未だわからない...。)
useSetRecoilState
フックを使うことで、新しい値をセットするための関数が返されます。これが、setNewAllTasks
関数です。
フォーム送信時にタスク追加を行うのですが、addTask
関数のところを見て頂くと、
// タスクを追加
const addTask = (data: AddTaskType): void => {
const taskTitle: string = data.title;
// selectorの実行
setNewAllTasks(taskTitle);
reset(); // フォームを空にする
}
setNewAllTasks
関数実行時に、引数として入力されたタスク名が渡されています。
これが、先程のselector
のset
プロパティに、引数title
として入っていき、selectortaskAddSelector
が実行されて、stateの更新が行われます。
AddForm
コンポーネントを表示するため、App.tsx
を次のようにします。
import { RecoilRoot } from 'recoil';
import AddForm from './components/AddForm';
import { contentsArea, title } from './styles/app';
const App = () => {
return (
<RecoilRoot>
<div>
<h1>ToDoリスト</h1>
<AddForm />
</div>
</RecoilRoot>
);
};
export default App;
3. タスクを一覧表示する
それでは、タスクを追加したので、一覧表示をしてみます。
次のように、List.tsx
ファイルにList
コンポーネントを作っていきます。
import { useRecoilValue } from 'recoil';
import { allTasksAtom } from '../recoil/recoilState';
import { AllTasksAtomType, TaskAtomType } from '../types/recoilStateType';
const List = () => {
const allTasks = useRecoilValue<AllTasksAtomType>(allTasksAtom);
return (
<div>
{allTasks.map((task: TaskAtomType, index: number) => (
<div key={index}>
<span>NO.{task.id}:</span>
<span>{task.title}</span>
</div>
))}
</div>
);
};
export default List;
タスクのidと名前を表示するようになっています。
一覧表示をするためには、タスク全体をまとめているstateの取得が必要です。stateの取得部分に注目してみます。
const allTasks = useRecoilValue<AllTasksAtomType>(allTasksAtom);
useRecoilValue
を用いて、タスク全体を配列としてまとめているatomallTasksAtom
の配列を取得しています。
取得した配列(allTasks
に入っている)を、map
関数で展開して表示しているのです。
List
コンポーネントを表示するため、App.tsx
を次のようにします。
import { RecoilRoot } from 'recoil';
import AddForm from './components/AddForm';
import List from './components/List';
import { contentsArea, title, tasksDisplayArea } from './styles/app';
const App = () => {
return (
<RecoilRoot>
<div>
<h1>ToDoリスト</h1>
<AddForm />
<div>
<List />
</div>
</div>
</RecoilRoot>
);
};
export default App;
ここで、タスク追加と一覧表示をやってみると、次のような状態になるかと思います。例えば、「あいうえお」と入れてみます。
無事、タスク追加されていますね。
4. 未完了のタスクと完了タスクを分けられるようにしておく
一覧で全て表示できても、未完了のタスクと完了タスクが混在してわかりにくいと思います。
ここで、未完了のタスクと完了タスクを分けて表示できるようにしましょう。
以下ような動きを想定して実装していきます。
- タスクが追加されると、「未完了のタスク」に表示される。
- 未完了のタスクが「完了」になれば、「未完了のタスク」エリアからは消えて、「完了したタスク」エリアに表示される。
- 完了したタスクが「未完了」になれば、「完了したタスク」エリアからは消えて、「未完了のタスク」エリアに表示される。
①完了状態の切り替えのselector
先に、完了状態を切り替える処理を用意しておきます。
完了状態を切り替えるボタンが押下されたタスクの完了状態が切り替わります。
以下のようなselector
を用意します。
// タスクの完了状態を切り替えるSelector
const changeTaskIsCompletedSelector = selector<AllTasksAtomType>({
key: 'changeTaskIsCompletedSelector',
get: ({ get }) => {
return get(allTasksAtom);
},
set: ({ get, set }, targetTaskId: any) => {
const targetId: number = targetTaskId; // 切り替え対象のタスクid
const newTasksArray: AllTasksAtomType = get(allTasksAtom).map((task: TaskAtomType) => {
if (task.id === targetId) {
// 切り替え対象のタスクの完了状態のboolean値を反対に切り替え。(eg: 現在true→false)
return {...task, isCompleted: !task.isCompleted}
} else {
return task;
}
});
set(allTasksAtom, newTasksArray);
}
});
こちらのselector
は実行時に、切り替えを行いたいタスクのidが渡ってきます。
map
関数を使って、渡されたidと一致するidのタスクであれば完了状態を切り替え、それ以外のタスクはそのままの状態で返しています。切り替えが行われた新しい配列(ここで言うnewTasksArray
)を返しているので、最後のset
関数で、引数に新しい配列newTasksArray
を渡して全タスクをまとめているatomallTasksAtom
を更新しています。
②完了状態に応じたタスクの取得を行うselector
未完了のタスクと完了タスクを分けて表示するには、完了状態に応じてタスクの取得を行います。
コンポーネントにて、タスクを全件取得してきてfilter
関数を使って取得する手段もありますが、今回はselector
を使っていきます。
次のように、未完了・完了タスク取得のselectorをそれぞれ作成してみます。
未完了のタスクを取得するselector
// 未完了のタスクを取得するSelector
const showTaskNotCompletedSelector = selector<AllTasksAtomType>({
key: 'showTaskNotCompletedSelector',
get: ({ get }) => {
const targetTasks: AllTasksAtomType = get(allTasksAtom).filter((task: TaskAtomType) => {
return task.isCompleted === false;
});
return targetTasks;
}
});
読み取り専用の値を返す、get
プロパティを使用します。
filter
関数を使って、get
関数でタスク全件を配列にまとめているatomallTasksAtom
から取り出したisCompleted
プロパティがfalse
のタスクのみを要素とする新しい配列を返しています。
同じく完了したタスクも取得できるようにしてみます。
完了したタスクを取得するselector
// 完了したタスクを取得するSelector
const showTaskCompletedSelector = selector<AllTasksAtomType>({
key: 'switchTaskCompletedSelector',
get: ({ get }) => {
const targetTasks: AllTasksAtomType = get(allTasksAtom).filter((task: TaskAtomType) => {
return task.isCompleted === true;
});
return targetTasks;
}
});
完了したタスクの場合は、filter
関数を使って、isCompleted
プロパティがtrue
のタスクのみを要素とする新しい配列を返しています。
③完了状態に応じたタスクの取得と表示
完了状態の切り替えと完了状態に応じたタスクの取得のselectorができました。
ここからは、それらを取得して表示・切り替え処理を実行できるように実装していきます。
未完了のタスク
未完了のタスクからやっていきます。
「未完了のタスク」エリアを作っていこうと思います。タスクが追加されると、まずここに表示されます。
次のように、NotCompletedList.tsx
ファイルにNotCompletedList
コンポーネントを作っていきます。
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { changeTaskIsCompletedSelector, showTaskNotCompletedSelector } from '../recoil/recoilState';
import { AllTasksAtomType, TaskAtomType } from '../types/recoilStateType';
import { listArea, title, itemsArea, item, toDoneButton } from '../styles/notCompletedList';
const NotCompletedList = () => {
const notCompletedTasks = useRecoilValue<AllTasksAtomType>(showTaskNotCompletedSelector);
const setChangeTaskIsCompleted = useSetRecoilState<any>(changeTaskIsCompletedSelector);
// タスクの完了状態を切り替える
const changeTaskIsCompleted = (id: number): void => {
setChangeTaskIsCompleted(id);
};
return (
<div>
<h2>未完了のタスク</h2>
<div>
{notCompletedTasks.map((task: TaskAtomType, index: number) => (
<div key={index}>
<p>
<span>NO.{task.id}:</span>
<span>{task.title}</span>
</p>
<button onClick={() => changeTaskIsCompleted(task.id)}>完了</button>
</div>
))}
</div>
</div>
);
};
export default NotCompletedList;
タスクのidと名前、完了状態を切り替えるボタンを表示します。
「完了」ボタンを押下すると、「未完了のタスク」エリアからは消えて「完了したタスク」エリアに表示されます。
まず、先程作ったselectorが実行される、完了状態の切り替え処理から見てみます。
const setChangeTaskIsCompleted = useSetRecoilState<any>(changeTaskIsCompletedSelector);
// タスクの完了状態を切り替える
const changeTaskIsCompleted = (id: number): void => {
setChangeTaskIsCompleted(id);
};
~略~
<button onClick={() => changeTaskIsCompleted(task.id)}>完了</button>
stateの更新を行うため、以下のようにuseSetRecoilState
を利用しています。
useSetRecoilState
フックを使うことで、新しい値をセットするための関数が返されます。これが、setChangeTaskIsCompleted
関数です。
タスクの完了状態を切り替えるためのchangeTaskIsCompleted
関数において、setChangeTaskIsCompleted
関数実行時に、引数としてidが渡されてきています。これは、クリックした(完了状態を切り替えたい)タスクのidです。
これが、先程のselectorchangeTaskIsCompletedSelector
のset
プロパティに、引数targetTaskId
として入っていき、selectorchangeTaskIsCompletedSelector
が実行されて、タスクの完了状態が切り替えられます。
「完了」ボタンを押下すると、このchangeTaskIsCompleted
関数が実行されるようになっています。
それでは、「未完了のタスク」エリアの表示部分について見てみます。
const notCompletedTasks = useRecoilValue<AllTasksAtomType>(showTaskNotCompletedSelector);
~略~
return (
<div>
<h2>未完了のタスク</h2>
<div>
{notCompletedTasks.map((task: TaskAtomType, index: number) => (
<div key={index}>
<p>
<span>NO.{task.id}:</span>
<span>{task.title}</span>
</p>
<button onClick={() => changeTaskIsCompleted(task.id)}>完了</button>
</div>
))}
</div>
</div>
);
未完了のタスクを取り出した配列を取得するselectorshowTaskNotCompletedSelector
で返される値を、useRecoilValue
フックで取得しています。配列で返されるので、map
関数で展開することで表示するようにしています。
ここで、再びタスク追加と表示を実行してみたいと思います。
先程は、List
コンポーネントを表示していましたが、未完了タスクを表示するNotCompletedList
コンポーネントを作ったので、不要になります。App.tsx
を次のように修正しておきましょう。
import { RecoilRoot } from 'recoil';
import AddForm from './components/AddForm';
import NotCompletedList from './components/NotCompletedList';
import { contentsArea, title, tasksDisplayArea } from './styles/app';
const App = () => {
return (
<RecoilRoot>
<div>
<h1>ToDoリスト</h1>
<AddForm />
<div>
<NotCompletedList />
</div>
</div>
</RecoilRoot>
);
};
export default App;
タスク追加をしてみると、次のような状態になりました。
新しく追加されたタスクが「未完了のタスク」エリアに表示されています。
完了したタスク
次に、完了したタスクについてもやっていきます。
「完了したタスク」エリアを作っていこうと思います。完了したタスクが、こちらのエリアに表示されます。
次のように、CompletedList.tsx
ファイルにCompletedList
コンポーネントを作っていきます。
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { changeTaskIsCompletedSelector, showTaskCompletedSelector } from '../recoil/recoilState';
import { AllTasksAtomType, TaskAtomType } from '../types/recoilStateType';
import { listArea, title, itemsArea, item, backToDoButton } from '../styles/completedList';
const CompletedList = () => {
const completedTasks = useRecoilValue<AllTasksAtomType>(showTaskCompletedSelector);
const setChangeTaskIsCompleted = useSetRecoilState<any>(changeTaskIsCompletedSelector);
// タスクの完了状態を切り替える
const changeTaskIsCompleted = (id: number): void => {
setChangeTaskIsCompleted(id);
};
return (
<div>
<h2>完了したタスク</h2>
<div>
{completedTasks.map((task: TaskAtomType, index: number) => (
<div key={index}>
<p>
<span>NO.{task.id}:</span>
<span>{task.title}</span>
</p>
<button onClick={() => changeTaskIsCompleted(task.id)}>未完了</button>
</div>
))}
</div>
</div>
);
};
export default CompletedList;
こちらも、タスクのidと名前、完了状態を切り替えるボタンを表示します。
「未完了」ボタンを押下すると、完了状態が未完了になるので、「完了したタスク」エリアからは消えて「未完了のタスク」エリアに表示されます。
タスクの完了状態を切り替えるためのchangeTaskIsCompleted
関数については、未完了のタスクを表示する箇所で説明したものと同じものなので、省きます。
「完了したタスク」エリアの表示部分について見てみます。
const completedTasks = useRecoilValue<AllTasksAtomType>(showTaskCompletedSelector);
~略~
return (
<div>
<h2>完了したタスク</h2>
<div>
{completedTasks.map((task: TaskAtomType, index: number) => (
<div key={index}>
<p>
<span>NO.{task.id}:</span>
<span>{task.title}</span>
</p>
<button onClick={() => changeTaskIsCompleted(task.id)}>未完了</button>
</div>
))}
</div>
</div>
);
完了したタスクを取り出した配列を取得するselectorshowTaskCompletedSelector
で返される値を、useRecoilValue
フックで取得しています。配列で返されるので、map
関数で展開することで表示するようにしています。
ここで、再びタスク追加と表示、そして完了状態の切り替えを実行してみたいと思います。
完了したタスクを表示するCompletedList
コンポーネントを作ったので、App.tsx
を次のように修正しておきましょう。
import { RecoilRoot } from 'recoil';
import AddForm from './components/AddForm';
import NotCompletedList from './components/NotCompletedList';
import CompletedList from './components/CompletedList';
import { contentsArea, title, tasksDisplayArea } from './styles/app';
const App = () => {
return (
<RecoilRoot>
<div>
<h1>ToDoリスト</h1>
<AddForm />
<div>
<NotCompletedList />
<CompletedList />
</div>
</div>
</RecoilRoot>
);
};
export default App;
タスク追加・完了状態切り替えを行なってみると、次のような状態になりました。
「完了」ボタンを押下したタスク「かきくけこ」が「完了したタスク」エリアに表示されています。
5. タスクを削除する
これで、タスクを追加して表示する、完了状態を切り替える処理は実装できました。最後に、タスクを削除する処理も実装しましょう。
「削除」ボタンが押下されたタスクが削除されて表示されなくなります。
以下のようなselector
を用意します。
// タスク削除のSelector
const deleteTaskSelector = selector<AllTasksAtomType>({
key: 'deleteTaskSelector',
get: ({ get }) => {
return get(allTasksAtom);
},
set: ({ get, set }, targetTaskId: any) => {
const targetId: number = targetTaskId; // 削除対象のタスクのid
// 削除対象のタスクのidとは異なるidのタスクのみを要素とする配列を返す
const deletedArray: AllTasksAtomType = get(allTasksAtom).filter((task: TaskAtomType) => {
return task.id !== targetId;
});
set(allTasksAtom, deletedArray);
}
});
引数として渡されているtargetTaskId
には、削除対象のタスクのidが入ってきます。
filter
関数を使って、get
関数でタスク全件を配列にまとめているatomallTasksAtom
から取り出した削除対象のタスクのidとは異なるidを持つタスクを要素とする新しい配列を返しています。
set
関数に注目してみます。
第1引数には、更新したいstate(atom
)のallTasksAtom
を渡します。そして、第2引数には、更新後の新しい値(上記で言う、対象のタスクが除かれた状態の配列)を渡します。
これにより、指定したstateに新しい値が書き込まれ、stateが更新されます。
それでは、タスク表示側の処理もやっていきます。
未完了のタスク
まずは、未完了のタスクから。次のように、NotCompletedList.tsx
を修正します。
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { changeTaskIsCompletedSelector, deleteTaskSelector, showTaskNotCompletedSelector } from '../recoil/recoilState';
import { AllTasksAtomType, TaskAtomType } from '../types/recoilStateType';
import { listArea, title, itemsArea, item, toDoneButton, deleteButton } from '../styles/notCompletedList';
const NotCompletedList = () => {
const notCompletedTasks = useRecoilValue<AllTasksAtomType>(showTaskNotCompletedSelector);
const setChangeTaskIsCompleted = useSetRecoilState<any>(changeTaskIsCompletedSelector);
const setDeleteTask = useSetRecoilState<any>(deleteTaskSelector);
~略~
// タスクを削除する
const deleteTask = (id: number): void => {
setDeleteTask(id);
};
return (
<div>
<h2>未完了のタスク</h2>
<div>
{notCompletedTasks.map((task: TaskAtomType, index: number) => (
<div key={index}>
<p>
<span>NO.{task.id}:</span>
<span>{task.title}</span>
</p>
<button onClick={() => changeTaskIsCompleted(task.id)}>完了</button>
{/* 「削除」ボタンを追加 */}
<button onClick={() => deleteTask(task.id)}>削除</button>
</div>
))}
</div>
</div>
);
};
export default NotCompletedList;
「削除」ボタンが追加されています。
先程作ったselectorが実行される処理を見ていきます。
const setDeleteTask = useSetRecoilState<any>(deleteTaskSelector);
~略~
// タスクを削除する
const deleteTask = (id: number): void => {
setDeleteTask(id);
};
return (
<div>
<h2>未完了のタスク</h2>
<div>
{notCompletedTasks.map((task: TaskAtomType, index: number) => (
<div key={index}>
<p>
<span>NO.{task.id}:</span>
<span>{task.title}</span>
</p>
<button onClick={() => changeTaskIsCompleted(task.id)}>完了</button>
{/* deleteTask関数実行 */}
<button onClick={() => deleteTask(task.id)}>削除</button>
</div>
))}
</div>
</div>
);
stateの更新を行うため、以下のようにuseSetRecoilState
を利用しています。
useSetRecoilState
フックを使うことで、新しい値をセットするための関数が返されます。これが、setDeleteTask
関数です。
「削除」ボタンを押下すると、タスク削除を行うdeleteTask
関数に対象のタスクのidが引数として渡され、setDeleteTask
関数に渡されてselectordeleteTaskSelector
が実行されます。これで、対象のidを持つタスクが削除されます。
完了したタスク
次は、完了したタスク。未完了のタスクと同じように、CompletedList.tsx
を修正します。
削除処理deleteTask
関数など全く同じです。
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { changeTaskIsCompletedSelector, deleteTaskSelector, showTaskCompletedSelector } from '../recoil/recoilState';
import { AllTasksAtomType, TaskAtomType } from '../types/recoilStateType';
import { listArea, title, itemsArea, item, backToDoButton, deleteButton } from '../styles/completedList';
const CompletedList = () => {
const completedTasks = useRecoilValue<AllTasksAtomType>(showTaskCompletedSelector);
const setChangeTaskIsCompleted = useSetRecoilState<any>(changeTaskIsCompletedSelector);
const setDeleteTask = useSetRecoilState<any>(deleteTaskSelector);
~略~
// タスクを削除する
const deleteTask = (id: number): void => {
setDeleteTask(id);
};
return (
<div>
<h2>完了したタスク</h2>
<div>
{completedTasks.map((task: TaskAtomType, index: number) => (
<div key={index}>
<p>
<span>NO.{task.id}:</span>
<span>{task.title}</span>
</p>
<button onClick={() => changeTaskIsCompleted(task.id)}>未完了</button>
<button onClick={() => deleteTask(task.id)}>削除</button>
</div>
))}
</div>
</div>
);
};
export default CompletedList;
未完了のタスク・完了したタスクそれぞれの表示で、削除処理の実装を行いました。
以下のような内容で、タスク追加〜タスクを削除を行なってみます。
- 追加するタスクは、「あいうえお」、「かきくけこ」、「なにぬねの」、「たちつてと」の4つ。
- 「かきくけこ」は完了。
- 未完了の「なにぬねの」は削除。
実行すると、次のような状態になりました。
-
タスクを追加
タスクが4つ、追加されています。 -
タスクを1つ完了する
タスク「かきくけこ」が完了して、「完了したタスク」のエリアに表示されています。 -
タスクを1つ削除する
タスク「なにぬねの」が削除されています。
これで、ToDoリストの基本的な機能が1通り実装できました🎉
お疲れ様でした〜!
おわりに
Reactで開発を行なっていると、stateをどう管理していくかが問題になることがあると思います。
Recoilは、stateをまとめて管理する必要がなく、atom
関数を使って複数管理できるという利点があったりします。
Recoilが提供している関数などは、他にもたくさんあるのですが、今回は基本的なものだけに絞りました。また機会があれば、今回紹介した以外のものについても使ってみたいです。
まだまだ使い慣れておらず、色々と不足している知識などあるかもしれないです。
認識の誤り・補足などがあれば、是非、コメントして頂けますと助かります〜🙇♀️
長文お読み下さり、ありがとうございました!
参考資料
Recoil 公式ドキュメント
Recoil 公式リポジトリ
株式会社ヌーラボHPヌーラボブログ Recoil Syncでさらに快適フロントエンド開発 #ヌーラボ真夏のブログリレー Recoilはいつまでexperimentalなのか?
When to use Writeable Selectors in RecoilJS
Facebook製の新しいステート管理ライブラリ「Recoil」を最速で理解する
【Recoil入門】Atom、useRecoilStateの使い方
Discussion