4
7

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.

Next.jsでTODOアプリを作成する日記②

Last updated at Posted at 2023-11-19

はじめに

こちらの記事でNext.jsの導入方法について教えて頂き、ローカル環境にNext.js + TypeScriptをセットアップする事ができました。

Next.js(Pages Router) + TypeScriptの環境構築

image.png
前回の続きを作っていきたいと思います。

前回までのあらすじ

1.Next.jsでTODOアプリを作成する日記①

  • Tailwindを導入できた
  • ComponentにPropsを渡せた
  • Componentの中でPropsを使用できた
  • Componentをループを使って表示できた
  • React Iconsを導入できた

2.Next.jsでTODOアプリを作成する日記②(この記事)

  • useStateを使って状態管理ができた
  • setStateを使って状態の更新ができた
  • 状態に対するイミュータブル(不変)な操作の必要性が分かった
  • Propsで渡された関数に引数を渡し親のイベントをトリガーできた

現状の画面はこんな感じです
image.png

FormからTodoを追加する

現状の画面からTODO追加のボタンを押すとFormに入力した情報から新しいTODOが作成される動きを実装したいです。

1.Form入力値を状態として保持する

React Hooksを使用してみようかと思います。
useStateを調べている時にReactでTODOアプリを作成しているZennの記事を発見しました。
分かりやすく勉強になるのですぐに私の日記よりこの記事を見て下さい。

フォームに入力された文字列を状態 (=state) として保持する

Formに入力された情報が変化した際、Todoオブジェクトに状態が反映されるようにしたいです。(UIとデータを紐づけたい)

    // fommTodo:状態を保持するための入れ物
    // setFormTodo:状態を変更するための関数 
	const [formTodo, setFormTodo] = useState<TodoItemProps>({
		title: "Hello",
		content: "World",
		status: "Done",
	});

Form入力が行われた際、引数のeventで入力情報を取得しsetFormTodoで状態を保持するハンドラを作成しました。

    // eventの型についてよく分からないですがform入力値はこういう型になるらしいです
	const handlerTodoTitleFormOnChange = (
		event: React.ChangeEvent<HTMLInputElement>
	) => {
        // Stateはイミュータブルな操作で変更する!
		const newTodo = { ...formTodo };
		newTodo.title = event.target.value;
		setFormTodo(newTodo);
	};

なぜわざわざnewTodoという新しいオブジェクトを作成するのか
その理由についてこちらでイミュータビリティ(immutability, 不変性)という言葉と共に非常に分かりやすく説明されています。

配列ステートの操作には要注意
※作成しているアプリもTODOアプリだし内容が完全に上位互換です

実際にフォームのOnChangeにハンドラを仕込み表示してみました。
2023-11-17_18h00_42.gif

useStateによる状態管理によりTodoオブジェクトの変化を感知してDOMの再レンダリングを行う事ができます。
image.png

todoForm.tsxは下記のようになりました。

components/Todo/todoForm.tsx
import React, { useState } from "react";
import { Todo } from "./todoItem";

const TodoForm = (): JSX.Element => {
	const [formTodo, setFormTodo] = useState<Todo>({
		title: "Hello",
		content: "World",
		status: "Done",
	});

	const handlerTodoTitleFormOnChange = (
		event: React.ChangeEvent<HTMLInputElement>
	) => {
		const newTodo = { ...formTodo };
		newTodo.title = event.target.value;
		setFormTodo(newTodo);
  };

  const handlerAddButtonOnclick = () => {
    
  }

	return (
		<div className="w-100 overflow-hidden bg-white rounded-lg shadow-md dark:bg-gray-800">
			<p>{formTodo.title}</p>
			<form onSubmit={(e) => e.preventDefault()}>
				<div className="m-2">
					<label className="text-gray-400">タイトル</label>
					<input
						type="text"
						value={formTodo.title}
						onChange={handlerTodoTitleFormOnChange}
						className="block w-full rounded-md border-0 py-1.5 px-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
					/>
				</div>

				<div className="m-2">
					<label className="text-gray-400">内容</label>
					<input
						type="text"
						value={formTodo.content}
						className="block w-full rounded-md border-0 py-1.5 px-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
					/>
				</div>
				<div className="m-2">
					<button
						type="submit"
						className="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
						TODO追加
					</button>
				</div>
			</form>
		</div>
	);
};

export default TodoForm;

配列ステートの操作には要注意 より

React はコンポーネントの変化をオブジェクトの同一性(差分)チェックで検知しています。
ミュータブルな操作をしてしまうとコピー元の情報も変更されてしまうため、変更前と変更後の差分を React が検知できなくなってしまいます。

現状のTODOアプリではFormの入力内容を状態として保持する必要はあまりないように思えますが、せっかく作ったのでこの方向性で行こうと思います。

2.子から親に値を渡してイベントを発生させる

FormのTODOを親のTodoListに追加する動きを実装したいです。

上記を実装する前にこれまでのコンポーネント構造を整理して以下のように変更しました。
には複数のとTodoを追加するためのを持たせ、全てのTodo情報を管理してもらおうかと思います。

image.png

FormのTODO追加ボタンを押すと

  1. 子側で状態管理しているTodoオブジェクトを親に渡す
  2. 親側で受け取ったTodoオブジェクトをTodoList(今表示されてるTodo達)に追加する
  3. TodoListの状態の変化を感知しDOM再のレンダリングを行う

こちらの記事がとても参考になりました。

React 子から親へ入力内容を渡す

上記を実装するために、親側で設定した関数を子へpropsとして渡します。
子側ではこの関数に引数として値を渡し使用する事で親側のイベントを発火させる事ができます。
また親側のTodoListの状態useStateで管理します。
これでtodoItemListの更新を感知してDOMの再描画が行われます。

components/Todo/Todot.sx
    // 受け取ったtodoを追加する関数を定義する
	const [todoItemList, setTodoList] = useState<Todo[]>([
		{
			title: "タイトル",
			content: "TODO内容はここに記載します。",
			status: "Done",
		},
		{
			title: "タイトル2",
			content: "TODO内容の二番目",
			status: "Progress",
		},
		{
			title: "タイトル3",
			content: "TODO内容の3番目",
			status: "Incomplete",
		},
	]);
     const addTodoOnClick = (todo: Todo) => {
        // 配列は直接操作せずコピーを作ってから
        const newTodoList = [...todoItemList];
    
        newTodoList.push(todo); // pushは元の配列を破壊するためコピーに対して使う
        setTodoList(newTodoList); // 
        console.log("追加");
    }

先ほどの配列ステートの操作には要注意と重複しますがここでも状態をイミュータブルに保つためにsetItemFormで配列を操作する時に注意が必要でした。

こちらの記事でイミュータブルな関数/ミュータブルな関数について分かりやすくまとめて頂いていますのでお借りします。

React + TypeScript: 状態に収めた配列を更新する

操作 非推奨(ミュータブル) 推奨(イミュータブル)
追加 push()、unshift() concat()、スプレッド構文...
削除 pop()、shift()、splice() filter()、slice()
置き換え splice()、インデックスへの代入 map()、reduce()
ソート reverse()、sort() 配列を複製して操作

次に上記で作成した関数を渡していきます。

components/Todo/todoList.tsx(親)
// 子に関数をpropsとして渡す
<TodoForm addTodoOnclick={addTodoOnClick} />
components/Todo/TodoForm.tsx(子)
// 子側で渡された関数を使ってハンドラを作成する
const handlerAddTodoOnclick = () => {
    props.addTodoOnclick(formTodo);
};
components/Todo/TodoForm.tsx(子)
// ButtonのOnClickに仕込む
<div className="m-2">
    <button
        ClassName={...}
        onClick={handlerAddTodoOnclick}>
        TODO追加
    </button>
</div>

やったー(^^)/
※TODO追加を押した後はFormを空にした方が良さそうなので修正しときます。
2023-11-20_00h38_39.gif

コード全体はこんな感じになっています。

components/Todo/todoListForm.tsx
import React, { useState } from "react";

import TodoItem, { Todo } from "./todoItem";
import TodoForm from "./todoForm";

const TodoListForm = (): JSX.Element => {
	const [todoItemList, setTodoList] = useState<Todo[]>([
		{
			title: "タイトル",
			content: "TODO内容はここに記載します。",
			status: "Done",
		},
		{
			title: "タイトル2",
			content: "TODO内容の二番目",
			status: "Progress",
		},
		{
			title: "タイトル3",
			content: "TODO内容の3番目",
			status: "Incomplete",
		},
	]);
	const addTodoOnClick = (todo: Todo) => {
		// const newTodoList = todoItemList.slice();
		const newTodoList = [...todoItemList];

		newTodoList.push(todo);
		setTodoList(newTodoList);
		console.log("追加");
	};

	return (
		<>
			{todoItemList.map((todo, i) => {
				return <TodoItem key={i} {...todo} />;
			})}
			<TodoForm addTodoOnclick={addTodoOnClick} />
		</>
	);
};

export default TodoListForm;

components/Todo/todoForm.tsx
import React, { useState } from "react";
import { Todo } from "./todoItem";

type TodoFormProps = {
	addTodoOnclick: (todo: Todo) => void;
};

const TodoForm = (props: TodoFormProps): JSX.Element => {
	const [formTodo, setFormTodo] = useState<Todo>({
		title: "Hello",
		content: "World",
		status: "Done",
	});

	const handlerAddTodoOnclick = () => {
		props.addTodoOnclick(formTodo);
	};

	const handlerTodoTitleFormOnChange = (
		event: React.ChangeEvent<HTMLInputElement>
	) => {
		const newTodo = { ...formTodo };
		newTodo.title = event.target.value;
		setFormTodo(newTodo);
	};

	const handlerTodoContentFormOnChange = (
		event: React.ChangeEvent<HTMLInputElement>
	) => {
		const newTodo = { ...formTodo };
		newTodo.content = event.target.value;
		setFormTodo(newTodo);
	};

	return (
		<div className="w-100 overflow-hidden bg-white rounded-lg shadow-md dark:bg-gray-800">
			<p>{formTodo.title}</p>
			<p>{formTodo.content}</p>
			<form onSubmit={(e) => e.preventDefault()}>
				<div className="m-2">
					<label className="text-gray-400">タイトル</label>
					<input
						type="text"
						value={formTodo.title}
						onChange={handlerTodoTitleFormOnChange}
						className="block w-full rounded-md border-0 py-1.5 px-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
					/>
				</div>

				<div className="m-2">
					<label className="text-gray-400">内容</label>
					<input
						type="text"
						value={formTodo.content}
						onChange={handlerTodoContentFormOnChange}
						className="block w-full rounded-md border-0 py-1.5 px-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
					/>
				</div>
				<div className="m-2">
					<button
						onClick={handlerAddTodoOnclick}
						className="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
						TODO追加
					</button>
				</div>
			</form>
		</div>
	);
};

export default TodoForm;

components/Todo/todoItem
import { FaCheckCircle } from "react-icons/fa";

export type Status = "Done" | "Progress" | "Incomplete" | "";

export type Todo = {
	title: string;
	content: string;
	status: Status;
};

const TodoItem = (props: Todo): JSX.Element => {
	// 状態に応じて各クラス名、テクストを取得する
	let statusClassName = {
		text: "",
		textColor: "",
		bgColor: "",
	};

	switch (props.status) {
		case "Done":
			statusClassName.text = "完了";
			statusClassName.textColor = "text-emerald-500";
			statusClassName.bgColor = "bg-emerald-500";
			break;
		case "Progress":
			statusClassName.text = "実行中";
			statusClassName.textColor = "text-blue-600";
			statusClassName.bgColor = "bg-blue-600";
			break;
		case "Incomplete":
			statusClassName.text = "未対応";
			statusClassName.textColor = "text-gray-500";
			statusClassName.bgColor = "bg-gray-600";
			break;
	}

	return (
		<div className="flex w-full border border-gray-300 max-w-sm overflow-hidden bg-white rounded-lg shadow-md dark:bg-gray-800">
			<div
				className={`flex items-center justify-center w-12 ${statusClassName.bgColor}`}>
				{props.status === "Done" && (
					<FaCheckCircle className="w-6 h-6 text-white fill-current" />
				)}
			</div>

			<div className="px-4 py-2 w-80">
				<div className="mx-3">
					<span className={`font-semibold ${statusClassName.textColor}`}>
						{statusClassName.text}
					</span>
					<p className="me-1 mb-0 text-gray-700">{props.title}</p>
					<span className="text-sm  text-gray-600 dark:text-gray-200 me-1">
						{props.content}
					</span>
				</div>
			</div>
		</div>
	);
};

export default TodoItem;

pages/index.tsx
import TodoForm from "@/components/Todo/todoForm";
import TodoListForm from "@/components/Todo/todoListForm";
import { Todo } from "@/components/Todo/todoItem";


export default function Home() {
	return (
		<>
			<div className="w-100 flex justify-center">
				<div className="my-5">
					<h1 className="text-xl font-bold text-green-400">Hello World</h1>
					<TodoListForm />
				</div>
			</div>
		</>
	);
}

まとめ

最初と比べて少しReactと仲良くなれたと感じます。
しかしまだReactの方はそうは思ってないと思います。

  • useStateを使って状態管理ができた
  • setStateを使って状態の更新ができた
  • 状態に対するイミュータブル(不変)な操作の必要性が分かった
  • 子側でpropsで渡された関数に引数を渡す事で親のイベントをトリガーできた

たくさんの方が投稿してくれた分かりやすい記事に感謝します。
非効率だったり推奨されていない書き方がありましたらまた教えて下さい<(_ _)>

③に続く Next.jsでTrello風タスク管理アプリを作成する日記③

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?