ReactでToDoリストCRUDアプリケーションを構築し、そのステートを管理する方法

複雑なステートをNextアプリケーションで管理する場合、すぐに難しくなることがあります。useStateのような従来のフックはステート管理に役立ちますが、プロップドリリングの問題が生じます。プロップドリリングとは、データまたは関数を複数のコンポーネントに渡すことを意味します。

より良いアプローチは、ステート管理ロジックをコンポーネントから分離し、アプリケーションのどこからでもこれらのステートを更新することです。コンテキストAPIを使用してシンプルなToDoリストアプリケーションを構築する方法について説明します。

ToDoリストを始める前に

ToDoリストアプリケーションを構築する前に、次のものが必要です。

  • 最新のJavaScript演算子とReactのuseStateフックの基本的な知識。
  • JavaScriptで配列とオブジェクトをデストラクチャリングする方法の理解。
  • ローカルマシンにNode v16.8以降がインストールされており、npmやyarnのようなパッケージマネージャーに慣れていること。

完成したプロジェクトはGitHubで参照および詳細な調査のために見つけることができます。

アプリケーションの状態と管理を理解する

アプリケーションの状態とは、特定の時点におけるアプリケーションの現在の状態を指します。これには、ユーザー入力やデータベースまたはAPI(Application Programming Interface)から取得されたデータなど、アプリが認識して管理する情報が含まれます。

アプリケーションの状態を理解するために、単純なカウンターアプリケーションの考えられる状態を考えてみましょう。これらには以下が含まれます。

  • カウンターがゼロのときのデフォルトの状態
  • カウンターが1増加したときの増加した状態
  • カウンターが1減少したときの減少した状態
  • カウンターがデフォルトの状態に戻るリセット状態

Reactコンポーネントは、状態の変更を購読できます。ユーザーがそのようなコンポーネントと対話すると、ボタンのクリックなどのアクションによって、状態の更新を管理できます。

このスニペットは、デフォルトの状態にある単純なカウンターアプリケーションを示しており、クリックアクションに基づいて状態を管理しています。

const [counter, setCounter] = useState(0);
return (
<main>
<h1>{counter}</h1>
<button onClick={() => setCounter(counter + 1)}>increase</button>
<button onClick={() => setCounter(counter - 1)}>decrease</button>
<button onClick={() => setCounter(0)}>reset</button>
</main>
);

セットアップとインストール

プロジェクトのリポジトリには、startercontextの2つのブランチがあります。スターターブランチをプロジェクトを構築するためのベースとして使用するか、コンテキストブランチを使用して最終的なデモをプレビューすることができます。

スターターアプリのクローン

スターターアプリは、最終的なアプリに必要なUIを提供するため、コアロジックの実装に集中できます。ターミナルを開き、次のコマンドを実行して、リポジトリのスターターブランチをローカルマシンにクローンします。

git clone -b starter https://github.com/makeuseofcode/Next.js-CRUD-todo-app.git

プロジェクトディレクトリ内で次のコマンドを実行して、依存関係をインストールし、開発サーバーを起動します。

yarn && yarn dev

または:

npm i && npm run dev

すべてがうまくいけば、UIがブラウザに表示されます。

ロジックの実装

Context APIは、手動のプロップドリリングを必要とせずに、コンポーネント間で状態データを管理および共有する方法を提供します。

ステップ1:コンテキストを作成してエクスポートする

src/app/contextフォルダーを作成してコンテキストファイルを格納し、プロジェクトディレクトリを整理します。このフォルダー内で、アプリケーションのすべてのコンテキストロジックを含むtodo.context.jsxファイルを作成します。

reactライブラリからcreateContext関数をインポートし、それを呼び出して、その結果を変数に格納します。

import { createContext} from "react";
const TodoContext = createContext();

次に、TodoContextをその使用可能な形式で返すカスタムuseTodoContextフックを作成します。

export const useTodoContext = () => useContext(TodoContext);

ステップ2:ステートを作成して管理する

アプリケーションのCRUD(作成、読み取り、更新、削除)アクションを実行するには、ステートを作成し、Providerコンポーネントで管理する必要があります。

const TodoContextProvider = ({ children }) => {
const [task, setTask] = useState("");
const [tasks, setTasks] = useState([]);
return <TodoContext.Provider value={{}}>{children}</TodoContext.Provider>;
};
export default TodoContextProvider;

returnステートメントの直前に、ユーザーがToDoに入力すると実行されるhandleTodoInput関数を作成します。この関数は、taskステートを更新します。

const handleTodoInput = (input) => setTask(input);

ユーザーがToDoを送信すると実行されるcreateTask関数を追加します。この関数は、tasksステートを更新し、新しいタスクにランダムなIDを割り当てます。

const createTask = (e) => {
e.preventDefault();
setTasks([
{
id: Math.trunc(Math.random() * 1000 + 1),
task,
},
...tasks,
]);
};

クリックされたタスクのIDと一致するIDを持つタスクをマッピングして更新するupdateTask関数を作成します。

const updateTask = (id, updateText) =>
setTasks(tasks.map((t) => (t.id === id ? { ...t, task: updateText } : t)));

与えられたパラメータと一致しないIDを持つすべてのタスクを含むようにtasksリストを更新するdeleteTask関数を作成します。

const deleteTask = (id) => setTasks(tasks.filter((t) => t.id !== id));

ステップ3:プロバイダーにステートとハンドラーを追加する

ステートを作成し、ステートを管理するためのコードを記述したら、これらのステートとハンドラー関数をProviderで利用できるようにする必要があります。Providerコンポーネントのvalueプロパティを使用して、オブジェクトの形式で提供できます。

return (
<TodoContext.Provider
value={{
task,
tasks,
handleTodoInput,
createTask,
updateTask,
deleteTask,
}}
>
{children}
</TodoContext.Provider>
);

ステップ4:コンテキストをスコープする

作成したProviderは、コンテキストをアプリケーション全体で利用できるようにするために、最上位コンポーネントをラップする必要があります。これを行うには、src/app/page.jsxを編集し、TodosコンポーネントをTodoContextProviderコンポーネントでラップします。

<TodoContextProvider>
<Todos />;
</TodoContextProvider>;

ステップ5:コンポーネントでコンテキストを使用する

src/app/components/Todos.jsxファイルを編集し、useTodoContext関数を呼び出して、tasks、task、handleTodoInput、createTaskをデストラクチャリングします。

const { task, tasks, handleTodoInput, createTask } = useTodoContext();

次に、送信イベントとメイン入力フィールドの変更を処理するようにフォーム要素を更新します。

<form onSubmit={(e) => createTask(e)}>
<input className="todo-input" type="text" placeholder="Enter a task" required value={task} onChange={(e) => handleTodoInput(e.target.value)} />
<input className="submit-todo" type="submit" value="Add task" />
</form>

ステップ6:UIでタスクをレンダリングする

これで、アプリを使用してタスクを作成し、tasksリストに追加できます。表示を更新するには、既存のtasksをマッピングしてUIでレンダリングする必要があります。まず、単一のToDoアイテムを保持するsrc/app/components/Todo.jsxコンポーネントを作成します。

src/app/components/Todo.jsxコンポーネント内で、src/app/context/todo.context.jsxファイルで作成したupdateTask関数とdeleteTask関数を呼び出して、タスクを編集または削除します。

import React, { useState } from "react";
import { useTodoContext } from "../context/todo.context";
const Todo = ({ task }) => {
const { updateTask, deleteTask } = useTodoContext();
// isEdit state tracks when a task is in edit mode
const [isEdit, setIsEdit] = useState(false);
return (
<table className="todo-wrapper">
<tbody>
<tr>
{isEdit ? ( <input type="text" value={task.task}
onChange={(e) => updateTask(task.id, e.target.value)} /> ) :
(<th className="task">{task.task}</th> )}
<td className="actions">
<button className="edit"
onClick={() => setIsEdit(!isEdit)}> {isEdit ? "Save" : "Edit"} </button>
<button onClick={() => deleteTask(task.id)}>Del</button>
</td>
</tr>
</tbody>
</table>
);
};
export default Todo;

taskに対してsrc/app/components/Todo.jsxコンポーネントをレンダリングするには、src/app/components/Todos.jsxファイルに移動し、header終了タグの直後にtasksを条件付きでマップします。

{tasks && (
<main>
{tasks.map((task, i) => ( <Todo key={i} task={task} /> ))}
</main>
)}

ブラウザでアプリケーションをテストし、期待どおりの結果が得られることを確認します。

ローカルストレージにタスクを保存する

現在、ページを更新するとタスクがリセットされ、作成したタスクがすべて破棄されます。この問題を解決する方法の1つは、ブラウザのローカルストレージにタスクを保存することです。

Web Storage APIは、ユーザーと開発者の両方にとってエクスペリエンスを向上させる機能を備えた、cookieストレージの改善版です。