Next.js の Todo アプリケーションを作成する方法

こんにちは、筆者の暇人です。

前回の記事:https://hima-tech-blog.jp/blog/6/ で Next.js を用いた RestFul な API を作成して、エンドポイントから JSON 形式のデータを取得する機構を作成しました。

今回は Todo アプリケーションを作る前に前回セットアップしたプロジェクトを使用して Todo アプリケーションを実装してみようと思います。

ただ、いきなりデータベースとの連携をしてアプリケーションを作成することは難しいかと思うので、また別にデータベースと連携するための ORM ライブラリの「Prisma」の使い方と
アプリケーションにどういう風に組み込んで作成するのかを別記事として紹介するので、よろしければ是非読んでみて手を動かして実装してみてください。

1. Next.js のプロジェクトのセットアップ

前回記事のhttps://hima-tech-blog.jp/blog/6/ で紹介しているので本記事の中では改めて説明はしませんので、プロジェクトのセットアップがわからない方は url から飛んで前回の記事をご覧ください。

2. ページの作成に関して

前回の記事では「App Router」の機能を使用してプロジェクトを作成したので、app ディレクトリ内に(domains)ディレクトリを作成します。 (ディレクトリ名)で作成されたディレクトリは Next.js では、ファイルベースのルーティングに組み込まれませんので、このディレクトリに特に意味はありませんが、「domains」という名前を付けているので、ここに作成していく url の名前と一致するフォルダを作成していくことがわかりやすくなるので、私はこのようなディレクトリ構成を採用しています。 そしたら次に(domains)ディレクトリの中に todo ディレクトリを作成して、さらに「components」ディレクトリと page.tsx ファイル、layout.tsx ファイルを作成していきます。

ここまで作成をするとこのような、構成になっているはずです。

.
└── src
    └── app
        └── (domains)
            └── todo
                ├── components
                ├── page.tsx
                └── layout.tsx

3. ルートセグメントに表示される UI について

「App Router」の機能を使用すると、URL パスに対応するセグメントと、app 以下のディレクトリ構成が一致するので、それを確認してみましょう。

ファイルベースのルーティングに関して、知りたい方は Next.js の公式ドキュメントである、https://nextjs.org/docs/app/building-your-application/routing/ の URL から飛べるページを読むと理解できるかと思います。

page.tsx が設置されているディレクトリが、ルートセグメントとして認識されるので、page.tsx 内にこのように記述してみましょう。

const Page = () => {
  return (
    <div>
      ここにはTodoアプリケーションのページを表示する予定です。
    </div>
  )
}

export default Page

続けて layout.tsx にも下記のように記述してみてください。

import { Metadata } from "next";

export const metadata: Metadata = {
  title: 'Todoアプリケーション',
}

const Layout = ({
  children
}: {
  children: React.ReactNode
}) => {
  return (
    <>
      {children}
    </>
  )
}

export default Layout

ここまで出来たらいったん、開発サーバーを立ち上げてルートセグメントにディレクトリが対応しているのかを確認してみましょう。下記のコマンドをターミナルから実行し、http://localhost:3000/todo/ をブラウザの url バーに入れてページを確認します。

$ npm run dev

そしてブラウザに http://localhost:3000/todo/ を入力します。 ここまで作成できれば、あとは React と同じように作成していき、アプリケーションの画面を作成します。 ただ、React と違う部分として、「Server Component」と「Client Component」の 2 種類がコンポーネントには存在しているので、その点だけ注意が必要になります。

今回は、サーバー側で作成して、描画するページは無いのですべて「Client Component」になりますが、実際にサーバーと通信してデータを取得するところは「Server Component」で取得することにより、アプリケーションのパフォーマンスが向上するなど、「Server Component」にはメリットが数多く存在するので、「Client Component」は React の Hooks である、useState や useEffect などのクライアントサイドでしか使用できない API を使用する際以外は極力利用を避けるようにしましょう。

なお、use client のディレクディブを付けないコンポーネントは、デフォルトで「Server Component」になるので、useState などのフックスを利用するときには気を付けましょう。

4. TodoItem コンポーネントを作成する

Todo を入力する際に、それを描画するコンポーネントを作成します。
具体的には入力フォームで、タイトルとタスクの詳細を記入したら、それを送信した際に画面上に表示されるものになります。
手書きのタスク管理入力とタスクリストのイメージ図を下に手書きで書いたものになりますので、「この部分」と記載されている部分を今からコンポーネントとして作成します。

alt text

作るものがイメージできたら、次に components ディレクトリ内に todoItem ディレクトリと todoItem.tsx を作成します。 現時点では下記のようなディレクトリ構成になっています。

.
└── src
    └── app
        └── (domains)
            └── todo
                ├── components
                │   └── todoItem
                │       └── todoItem.tsx
                ├── page.tsx
                └── layout.tsx

そうしたら、todoItem.tsx ファイル内に下記のコードを書き込みます。

import { TodoType } from "../todoPage/todoPage"

export const TodoItem = ({
  todo,
  index,
  deleteTodo,
  toggleTodoShow
}: {
  todo: TodoType;
  index: number;
  deleteTodo: (id: number) => void;
  toggleTodoShow: (id: number) => void;  // 追加
}) => {
  return (
    <div className="w-full flex gap-x-10">
      <div>
        {todo.isShow ? <del>タイトル:{todo.title}</del> : `タイトル:${todo.title}`}
      </div>
      <div className="flex-1">
        {todo.isShow ? <del>詳細:{todo.description}</del> : `詳細:${todo.description}`}
      </div>
      <div className="flex gap-x-4">
        <button onClick={() => deleteTodo(index)}>削除</button>
        <button onClick={() => toggleTodoShow(index)}>完了</button> {/* 修正 */}
      </div>
    </div>
  );
};

export default TodoItem

現時点では import {TodoType} from “../todoPage/todoPage”が作成されていないモジュールを参照しているので、エラーになるかと思いますが、気にせず次の todoPage を作成します。

todoPage コンポーネントは、todo にルーティングした際に、描画される画面のコンポーネントになります。
つまり、入力フォームと TodoItem のコンポーネントがすべてが描画されるコンポーネントとなります。

5. 全体ページの作成

次は同じように component ディレクトリ内に todoPage ディレクトリと todoPage.tsx ファイルを作成します。

.
└── src
    └── app
        └── (domains)
            └── todo
                ├── components
                │   ├── todoItem
                │   │   └── todoItem.tsx
                │   └── todoPage
                │       └── todoPage.tsx
                ├── page.tsx
                └── layout.tsx

上記の構成で作成できたら、「todoPage.tsx」ファイルに以下のように書き込みます。

'use client'

import { useState } from "react"
import TodoItem from "../todoItem/todoItem"

export type TodoType = {
  title: string
  description: string
  isShow: boolean
}

export const TodoPage = () => {
  const [title, setTitle] = useState<string>("");
  const [description, setDescription] = useState<string>("");
  const [todo, setTodo] = useState<TodoType[]>([]);

  const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setTitle(e.target.value);
  };

  const handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setDescription(e.target.value);
  };

  const handleClick = () => {
    setTodo([{ title: title, description: description, isShow: false }, ...todo]);
  };

  const handleTodoDelete = (id: number) => {
    const array = todo.slice();
    array.splice(id, 1);
    setTodo(array);
  };

  const toggleTodoShow = (id: number) => {
    const newTodo = todo.map((item, index) => {
      if (index === id) {
        return { ...item, isShow: !item.isShow };
      }
      return item;
    });
    setTodo(newTodo);
  };

  return (
    <div className="w-full flex flex-col">
      <div className="flex flex-col w-full gap-y-4">
        <div className="w-full">
          <label className=" min-w-80" htmlFor="title">
            Todoタイトル:
          </label>
          <input
            className="text-black"
            value={title}
            onChange={handleTitleChange}
          />
        </div>
        <div className="w-full">
          <label className=" min-w-80" htmlFor="description">
            詳細:
          </label>
          <input
            className="text-black"
            value={description}
            onChange={handleDescriptionChange}
          />
        </div>
        <button className="cursor-pointer" onClick={handleClick}>
          送信
        </button>
      </div>
      <div className="w-full flex flex-col gap-y-8">
        {todo.map((item, index) => {
          return (
            <TodoItem
              todo={item}
              index={index}
              deleteTodo={handleTodoDelete}
              toggleTodoShow={toggleTodoShow}
              key={index}
            />
          );
        })}
      </div>
    </div>
  );
};

最初に書いてある「’use client’」ディレクティブは Client Component を使用するという宣言になります。

詳しく知りたい方は、https://ja.react.dev/reference/rsc/use-client/ React の公式ドキュメントを読むと詳しく理解することができます。
とりあえずアプリを動かしてみたいという方は、ブラウザ上で動作する機能を利用してるから use client を付けてるんだなーぐらいの認識で OK です。

そうしたら page.tsx を少し変えていきます。

page.tsx

import { TodoPage } from "./components/todoPage/todoPage"

const Page = () => {
  return <TodoPage />
}

export default Page

page.tsx で実際に描画したいコンポーネントを 1 つだけおいてレンダリングをかけるようにしておきます。 なぜこのようなことをしているのかというと、page.tsx までは server でレンダリングされ、各コンポーネントがツリー構造で読み込まれていくときに、途中に Client Component が読み込まれて、ブラウザ側で描画が走っているという理解のためだけに分けて書いています。

Next.js の描画について知りたい人は、Next.js の公式ドキュメントであるhttps://nextjs.org/docs/app/building-your-application/rendering/ から「Server Components」と「Client Components」の記事を読むとレンダリングの方式に関して理解ができると思うので、一読されることをお勧めします。(リクエストがあればここら辺の解説記事も執筆しようと思います。)

6.アプリケーションの起動

ここまで記事通りに進められているのであれば、アプリケーションを開発サーバーで起動して、Todo アプリケーションを動かすことができる状態になっています。
下記のコマンドをターミナルに入力して、確かめてみましょう。

$ npm run dev

開発サーバーが立ち上がったら、ブラウザに「http://localhost:3000/todo/」と入力すると、下の画面が立ち上がるはずです。(筆者の場合には本ブログ内に直接コードを打ち込んでいるため、微妙に画面のデザインが違います)

alt text

上記の画面は自分のブログサイトにコードを書いてページを作成しているので、見た目がダークモード対応していて黒いですが、基本的には、タイトルと詳細を入力し、送信ボタンが一つあり、その下に送信されたタスクが表示されています。CSS は Tailwind CSS を利用していますが、ほぼ装飾をしていないので、見た目が非常に悪いと思いますが、気になる方は、今度「Tailwind CSS」のレイアウト記事を書くので、自分好みのサイトを作成してみてください。

7.最後に

現時点では、ページ内に一時的に描画しているだけであり、どこかに保存していないため、完全にフロントエンドを理解するためだけのページになってしまっています。

また、少し記事が長くなってしまったため、コードの解説等は全くしていないため、なぜこのような動きをするのかを理解できないという方も少なからずいらっしゃると思いますので、近日中にコードの解説記事を追加するか、この記事をアップデートしようと思います。

次回はデータベースを操作する ORM ライブラリの「Prisma」を解説し、「Route Handler」と組み合わせた Rest API を作成し、データの入出力をする機構を作成していきたいと思います。

また、リレーショナルデータベースは「正規化」をすることにより、データを無駄なく、矛盾なく取り扱うことができるので(パフォーマンスの関係で最低限しかしないこともあるのですが)、正規化も学習記事としてそのうち取り扱います。

最後まで読んでいただきありがとうございました。