React100本ノックやってみた【03】

この記事は以下の記事を参考にして作成しました。

https://qiita.com/Sicut_study/items/5dec3c42c8f3c90f0487

## **問題**

TODOリストアプリを作成する

## **目的**

React の基本を理解する: カウンターアプリは、ステート管理やイベントハンドリングなど、React の基本的な機能を学ぶのに適しています。初心者にとっては、これらの概念の理解を深める良いスタートポイントとなります。

## **達成条件**

1. 入力フィールド: ユーザーがタスクを入力するためのテキストフィールドが存在する。タスク名が1文字以上でない場合はバリデーションする
2. 追加ボタン: 入力されたタスクをTODOリストに追加するためのボタン。
3. タスクリスト表示: 追加されたタスクがリストとして表示される。
4. 削除ボタン: 各タスクの隣には削除ボタンがあり、それをクリックすることで該当のタスクをリストから削除する

## **実際に解いてみた**

### **利用技術**

- React
- TypeScript
- shudcn/ui
- TailwindCSS
- Next.js

## 結果



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3993206/7b6578e4-bc12-62f4-e5cb-2d93029311b6.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3993206/e6f1ab89-41b9-301d-03e4-9f7c23ab4b7a.png)


## ディレクトリ構成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3993206/48d159a4-f961-7b7d-0c32-1f2f5a43614d.png)

## コード


page.tsx

```tsx
'use client';

import Add from '@/components/add';
import List from '@/components/list';
import { useState } from 'react';

export type Task = {
  //typeScriptでは自分で型に名前を付けられる
  name: string;
  isDone: boolean;
};

export default function Home() {
  const [todoList, setTodoList] = useState<Task[]>([
    //const [state, setState] = useState<型>(初期値);
    //①
    //todoListという名前のTask型の配列（今回はconstと前につけてるので定数）を宣言
    {
      name: '筋トレ',
      isDone: false,
    },
    {
      name: '買い物',
      isDone: true,
    },
  ]);

  function addTodo(task: Task) {
    setTodoList([...todoList, task]);
  }

  // タスクの完了状態を切り替える関数を追加
  function toggleTaskStatus(index: number) {
    const newTodoList = todoList.map((task, i) => {
      if (i === index) {
        return { ...task, isDone: !task.isDone };
      }
      return task;
    });
    setTodoList(newTodoList);
  }

  // タスクを削除する関数を追加
  function deleteTask(index: number) {
    const newTodoList = todoList.filter((_, i) => i !== index);
    setTodoList(newTodoList);
  }

  return (
    <div>
      <List todoList={todoList} onToggleStatus={toggleTaskStatus} onDelete={deleteTask} />
      <Add onSubmit={addTodo} />
    </div>
  );
}

```

components/add/index.tsx

```jsx
'use client';

import {
  Dialog,
  DialogContent,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Plus } from 'lucide-react';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';

import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
import { Input } from '../ui/input';

type Task = {
  name: string;
  isDone: boolean;
};

type AddProps = {
  onSubmit: (task: Task) => void; //void:関数が呼ばれた後に返り値はないということ
};

const formSchema = z.object({
  username: z.string().min(2, {
    message: 'タスク名を入力してください',
  }),
});

export function ProfileForm({ onSubmit }: AddProps) {
  const form = useForm<z.infer<typeof formSchema>>({
    //useformはReact Hook Form のメイン関数で、フォームの状態と操作を管理
    //typeof:実際の値やオブジェクトからその型を取得。
    //infer:条件付き型の中で部分的に型を推論。
    //つまり、formという変数をformSchemaから推測した型で定義する
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: '',
    },
  });

  function onSubmitForm(values: z.infer<typeof formSchema>) {
    // タスクを作成して親コンポーネントに渡す
    onSubmit({
      name: values.username,
      isDone: false,
    });
    form.reset(); // フォームをリセット
  }

  return (
    <Form {...form}>
      {/*useForm によって生成されたフォーム関連の情報（例: form.control, form.handleSubmit）をFormに渡しています。 */}
      <form onSubmit={form.handleSubmit(onSubmitForm)} className="space-y-8">
        {/*formはhtmlのformタグである。onSubmit属性はHTMLのformタグに使われ、フォームが送信される際に実行する関数（イベントハンドラー）を指定

        form.handleSubmit(onSubmitForm)
        →formはuseForm フックの戻り値（オブジェクト）であり、その中のhandleSubmitを今回使う。
        handleSubmit=onSubmit の前にバリデーションを加えるために使う。フォームの送信時に呼び出される関数をラップし、バリデーションが成功した場合のみその関数を実行
        に指定されたJavaScript関数を実行する。(今回はonSubmitForm関数を実行)*/}
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => (
            <FormItem>
              <FormControl>
                <Input placeholder="タスクを追加" {...field} />
              </FormControl>

              <FormMessage />
            </FormItem>
          )}
        />
        <DialogFooter>
          <Button type="submit">保存</Button>
        </DialogFooter>
      </form>
    </Form>
  );
}

export default function Add({ onSubmit }: AddProps) {
  return (
    <div>
      <Dialog>
        <DialogTrigger asChild>
          <Button className="fixed bottom-8 right-8 size-14 rounded-full" size="icon">
            <Plus />
          </Button>
        </DialogTrigger>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>新規登録</DialogTitle>
            <ProfileForm onSubmit={onSubmit} />
          </DialogHeader>
        </DialogContent>
      </Dialog>
    </div>
  );
}

```

components/list/index.tsx

```tsx
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
import { Button } from '../ui/button';
import { Trash } from 'lucide-react';
import { Task } from '@/app/page';

type Props = {
  //Home（）からTask型の配列を受け取りたいので、引数（propsという）の型を定義する
  todoList: Task[]; //todoListという名前でTask型の配列を受け取る
  onToggleStatus: (index: number) => void;
  onDelete: (index: number) => void; // 削除用の関数を追加
};

export default function List({ todoList, onToggleStatus, onDelete }: Props) {
  //ListというコンポーネントでProps型の{ todoList }というpropsを受け取る
  return (
    <div className="flex h-svh w-screen items-center justify-center">
      <Card className="w-10/12 max-w-md overflow-hidden">
        <CardHeader className="bg-muted-foreground text-muted">
          <CardTitle>TODO List</CardTitle>
        </CardHeader>
        <CardContent className="flex flex-col gap-4 py-12">
          {todoList.map((task, index) => (
            //.map() を使って、todoList の各タスク (task) をレンダリング
            //index は現在のタスクの配列内の位置
            <div className="flex items-center justify-between space-x-2" key={index.toString()}>
              <div className="space-x-2">
                <Checkbox
                  id={`task-${index}`} // 各タスクに一意のIDを付与
                  checked={task.isDone}
                  onCheckedChange={() => onToggleStatus(index)}
                />
                <label
                  htmlFor={`task-${index}`}
                  className="text-xl font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
                >
                  {task.name}
                </label>
              </div>
              <Button
                variant="outline"
                size="icon"
                className="rounded-full hover:text-destructive"
                onClick={() => onDelete(index)}
              >
                <Trash />
              </Button>
            </div>
          ))}
        </CardContent>
      </Card>
    </div>
  );
}

```

## 感想



typescriptの知識があまりないまま作成した、UseStateやmap関数についてあやふやだったため作成がとても大変だった。ボタンを押してリストをtodoに追加する機能はAIに聞いて作ってもらったため理解のあいまいな部分が多い。そのため自力で作れるようになるまでまた何回か挑戦する。