0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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

問題

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

目的

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

達成条件

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

実際に解いてみた

利用技術

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

結果

image.png

image.png

ディレクトリ構成

image.png

コード

page.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

'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

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に聞いて作ってもらったため理解のあいまいな部分が多い。そのため自力で作れるようになるまでまた何回か挑戦する。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?