この記事は以下の記事を参考にして作成しました。
問題
TODOリストアプリを作成する
目的
React の基本を理解する: カウンターアプリは、ステート管理やイベントハンドリングなど、React の基本的な機能を学ぶのに適しています。初心者にとっては、これらの概念の理解を深める良いスタートポイントとなります。
達成条件
- 入力フィールド: ユーザーがタスクを入力するためのテキストフィールドが存在する。タスク名が1文字以上でない場合はバリデーションする
- 追加ボタン: 入力されたタスクをTODOリストに追加するためのボタン。
- タスクリスト表示: 追加されたタスクがリストとして表示される。
- 削除ボタン: 各タスクの隣には削除ボタンがあり、それをクリックすることで該当のタスクをリストから削除する
実際に解いてみた
利用技術
- React
- TypeScript
- shudcn/ui
- TailwindCSS
- Next.js
結果
ディレクトリ構成
コード
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に聞いて作ってもらったため理解のあいまいな部分が多い。そのため自力で作れるようになるまでまた何回か挑戦する。