【Next.js】定番のTodoアプリを作ってみる

## はじめに
プログラミングの勉強をする際、何か作ってみる題材として定番中の定番はTodoアプリですよね。
僕も例に漏れず、個人学習をするならまずはTodoアプリ作ってみるか…と思ったので作ってみることにしました。


## ルール
・Next.js + typescript + tailwindCSSを使う
・フロントエンドのみの実装で完結させる
・機能は最小限（まずは完成させるのが大事！）

## 仕様
・タスクを入力する欄がある
・タスクを入力してエンターを押したら入力欄の下にリスト形式で表示させる。
・タスク一覧のアイテムは完了したら削除できるようにする。

めっちゃくちゃシンプルですね笑
でも最初開発に着手する時には、これくらい最低限の機能を目指して実装した方がよいかと思います！
途中で放り出さないのが大事。

## 開発準備
では早速実装していきます！
まずはお決まりの`create-next-app`をします。
せっかくなのでApp Router仕様で行きましょう。（ver13.4から追加された新しいルーティング仕様です）
```
$ npx create-next-app 【プロジェクト名】
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577451/3cd43e0e-c60d-4ccc-20e0-7200a396f00f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577451/946be599-a97a-2157-666b-b60a13efd2a1.png)

はい、これでNext.jsで開発するための用意が整いました。お手軽過ぎてびっくりです。

## 実装したソースコード
急に完成まで飛んでしまいますが、こちらが出来上がったコードになります。

```page.tsx
import React from 'react';
import TodoList from '../components/TodoList';

// Todoアプリの中身
const Home = () => {
    return (
        <div className="container mx-auto p-8 text-center max-w-2xl">
            <h1 className="text-2xl mb-4">Todo App</h1>
            <TodoList />
        </div>
    );
};

export default Home;
```

```TodoList.tsx
// クライアントコンポーネント
"use client" // ←※※注意ポイント①※※

// 必要なライブラリとコンポーネントをインポート
import React, { useState } from 'react';
import TodoItem from './TodoItem';

const TodoList = () => {
    // タスクと新しいタスク入力を管理するためのuseState
    const [tasks, setTasks] = useState<Array<{ task: string; completed: boolean }>>([]); // ←※※注意ポイント②※※
    const [newTask, setNewTask] = useState('');

    // タスク配列に新しいタスクを追加する関数
    const addTask = () => {
        // 新しいタスク入力が空でないことを確認
        if (newTask.trim()) {
            // 新しいタスクでタスク配列を更新
            setTasks([...tasks, { task: newTask, completed: false }]);
            // 新しいタスク入力フィールドをリセット
            setNewTask('');
        }
    };

    // タスクを削除する
    const removeTask = (index: number) => {
        // タスク配列のコピーを作成
        const newTasks = [...tasks];
        // タスク配列から指定されたタスクを削除
        newTasks.splice(index, 1);
        // タスク配列のステートを更新
        setTasks(newTasks);
    };

    return (
        <div className="p-4">
            {/* 新しいタスクを追加するための入力フィールド */}
            <input
                type="text"
                placeholder="Add a task"
                value={newTask}
                onChange={(e) => setNewTask(e.target.value)}
                onKeyDown={(e) => e.key === 'Enter' && addTask()}
                className="p-2 border rounded w-full mb-8"
            />
            {/* タスクのリストをレンダリング */}
            <div>
                {tasks.map((task, index) => (
                    <TodoItem
                        key={index} // ←※※注意ポイント③※※
                        task={task.task}
                        // removeTask関数をTodoItemに渡す
                        toggleCompletion={() => removeTask(index)}
                    />
                ))}
            </div>
        </div>
    );
};

export default TodoList;
```

```TodoItem.tsx
// 必要なライブラリをインポート
import React from 'react';

const TodoItem = (props: { task: string; toggleCompletion: () => void; }) => { // ←注意ポイント④
    return (
        <div className="flex justify-between p-2 border-b bg-white mb-5">
            {/* タスクテキストを表示 */}
            <span className="flex-1">{props.task}</span>
            {/* toggleCompletion関数をトリガーするボタン */}
            <button onClick={props.toggleCompletion} className="text-xl">
                ✓
            </button>
        </div>
    );
};

export default TodoItem;
```

```globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

body {
  background-color: #eaeaea;
}
```

▼フォルダ構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577451/c0d94156-80e4-4c60-f966-46bf0f188438.png)


▼完成画面
![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577451/6d672d5b-6266-78cc-acd3-93ef829583aa.gif)


## 注意ポイント
実装してみて何点か注意するポイントがあったのでそちらの説明だけします！
ソースコード上に「※※注意ポイント~※※」とコメントしてある箇所です。


### 注意ポイント1
TodoList.tsxの先頭で"use client"と記述がありますね。
こちらはこのコンポーネントはクライアントコンポーネントだよと宣言しているという意味になります。

………………クライアントコンポーネントとは???

僕は頭が悪いので（悲）ふわっと以下のように理解しました。

・**Server Components**と**Client Components**という概念がある
・**Server Components**はサーバーサイドでレンダリングされ、**Client Components**はクライアントサイドでレンダリングされるコンポーネントである。
・Next.js13ではデフォルトでコンポーネントはすべて**Server Components**になっているため**Client Components**にするには宣言が必要
・もちろん両者で出来ることは違っていて、**Server Components**ではuseStateが実行できない。

Next.jsを扱う上でこの概念の理解は必須らしいので皆さん頑張って理解してください（ネット上に先達の素晴らしい記事がたくさん転がっているのでそれ見て理解しましょう）

はい、ここで気を付けることはTodoList.tsxの先頭で"use client"と宣言しないとこのコンポーネント内でuseStateが使えないということです。
Reactのノリで書いてたらclientがどうのこうのエラーが出て戸惑いました。
useStateだけでなくuseEffectなどもClient Componentsにしないと実行できないので注意してください。

▼参考になった記事
[【Next.js】Server ComponentsとClient Componentsについてまとめてみた](https://suzumura-tumiage.com/javascript/598/#toc10)



### 注意ポイント2
```TodoList.tsx
const [tasks, setTasks] = useState<Array<{ task: string; completed: boolean }>>([]);
```
ここではuseStateの宣言をする箇所、なんかぐちゃぐちゃしていますね。
Typescriptでは型の指定をしないといけないとはいえ、プリミティブ型の値に関しては型推論を行ってくれるのですが、配列は型を明示しておかないとエラーになってしまいます。
そのため配列の場合は型を明示するようにしましょう。

▼参考になった記事
[useStateの空配列に型をつける](https://zenn.dev/kaz_z/articles/usestate-type)

### 注意ポイント3
Todoリストのように何個も同じような要素を作成する場合、要素にkeyを付けてあげるようにしましょう。
なくても動くのですが、パフォーマンスが低下します。

パフォーマンスが低下するって具体的にどういうこと？？と深堀りされると「うっ」となりますが、keyをつけてあげないとReactさん（Nextさん）が要素をそれぞれ判別できないから一個追加したり削除したりするたびに全部再レンダリングすることになっちゃう、、、という認識でよいかと思います。

▼参考になった記事
[【React】 リストをレンダリングする際のkeyの必要性](https://progtext.net/programming/react-key/)


### 注意ポイント4
ここではpropsとして関数を渡してあげてます。
ここで出てくるvoidとは何ぞや…？って方もいるかもしれないので一応補足しようかと思います！

voidとは一言で説明すると「何も返さないよん」な返り値の型です。
toggleCompletionにはremoveTask関数が入ってきて、ボタンをクリックしたらタスクを削除する関数が渡されています。
このremoveTask関数は返り値が存在しないですよね。
そのためtoggleCompletionには返り値が発生しない関数を渡すよ～っていう意味でこのような書き方になっています。

▼参考になった記事
[Next.js と TypeScript で、void 型を理解する](https://commte.net/nextjs-typescript-void)


## 最後に
というわけで簡単なTodoアプリを実装してみました。
必要最低限な機能しか実装しないとはいっても調べないといけない箇所も多かったので、やはりどんな簡単なものでも作ってみるのが一番勉強になるなあと感じました。
せっかく作ったのでこの後はAWSにアップしたり、追加機能の実装もしようかなと思っています。
派生した作業に手っ取り早く着手できるのも、簡単なTodoアプリを作るメリットですね。

では、皆さんよい個人開発ライフをお送りください！( ˘ω˘ )