Next.jsでTodoリストを作った!
setting
daisyuiとreact iconsを使うのでinstallします!
npm i -D daisyui@latest
npm install react-icons --save
daisyuiはinstallしたあとtailwind.config.ts
に以下を追加します。
plugins: [require("daisyui")],
markup
taskを追加するボタンをdaisyuiのcomponentで作ります!
<div>
<button className="btn btn-primary w-full">
Add new task
<AiOutlinePlus className="ml-2" size={18} />
</button>
</div>
リストを追加したらデーターが入るtableを作ります。
daisyuiでコビーしてもってきました!
<div className="overflow-x-auto">
<table className="table">
<thead>
<tr>
<th>Name</th>
<th>Job</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cy Ganderton</td>
<td>Blue</td>
</tr>
</tbody>
</table>
</div>
api
簡単にREST APIを構築するため、json-serverを使います!
npm i -D json-server
json-server --watch data/todos.json --port 9000
const baseUrl = "http://localhost:9000";
export const getAllTodos = async (): Promise<ITask[]> => {
const res = await fetch(`${baseUrl}/tasks`);
const todos = await res.json();
return todos;
};
TypeScriptなのでinterfaceを設定して型を与えます。
export interface ITask {
id: string;
text: string;
}
const tasks = await getAllTodos();
console.log(tasks);
consoleで確認してみます!
type設定
TodoList
にap情報を渡すときtypeのエラーが発生します。
componentの内部でもtypeを設定したらエラーが消されます!
const tasks = await getAllTodos();
console.log(tasks);
return (
<main className="max-w-4xl mx-auto mt-4">
<div className="text-center my-5 flex flex-col gap-4">
<h1 className="text-2xl font-bold">Todo List App</h1>
<AddTask />
</div>
<TodoList tasks={tasks} />
</main>
type.tsで設定したtypeをもってきます
interface TodoListProps {
tasks: ITask[];
}
const TodoList: React.FC<TodoListProps> = () => {
...
}
Task component化
TodoList.tsx
のtask部分をコンポネート化しましょう!
<tbody>
{tasks.map((task) => (
<Task key={task.id} task={task} />
))}
</tbody>
interface TaskProps {
task: ITask;
}
const Task: React.FC<TaskProps> = ({ task }) => {
return (
<tr key={task.id}>
<td>{task.text}</td>
<td>Blue</td>
</tr>
);
};
TaskPropsでITaskを入れます。ここでは配列ではないので後ろの[]Arrayを消します!
Modal component作成
daisyuiでmadalをもってきます!
タスクを追加したいときmodalから追加することにしたいのでAddTask
にModalコンポーネントを追加します!
const [modalOpen, setModalOpen] = useState<boolean>(false);
return (
<div>
<button
onClick={() => setModalOpen(true)}
className="btn btn-primary w-full"
>
Add new task
<AiOutlinePlus className="ml-2" size={18} />
</button>
<Modal modalOpen={modalOpen} setModalOpen={setModalOpen} />
</div>
);
interface ModalProps {
modalOpen: boolean;
setModalOpen: (open: boolean) => boolean;
}
const Modal: React.FC<ModalProps> = ({ modalOpen, setModalOpen }) => {
return (
<dialog
id="my_modal_3"
className={`modal ${modalOpen ? "modal-open" : ""}`}
>
<div className="modal-box">
<form method="dialog">
<button
onClick={() => setModalOpen(false)}
className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
>
✕
</button>
</form>
...
</dialog>
);
};
export default Modal;
AddTask.tsxでエラーが出ますが、useStateみたいなReactHooksはserverで使えないからです。
clien側で使うため"use client"
;を書きます!
add todolist(タスク追加)
リストを追加するためaddTodo
APIを作ります。
export const addTodo = async (todo: ITask): Promise<ITask> => {
const res = await fetch(`${baseUrl}/tasks`, {
method: "POST",
headers: {
"Content-type": "application/json",
},
body: JSON.stringify(todo),
});
const newTodo = await res.json();
};
const AddTask = () => {
const router = useRouter();
const [modalOpen, setModalOpen] = useState<boolean>(false);
const [newTaskValue, setNewTaskValue] = useState<string>("");
const handleSubmitNewTodo: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
await addTodo({
id: uuidv4(),
text: newTaskValue,
});
setNewTaskValue("");
setModalOpen(false);
router.refresh();
};
return (
<div>
<button
onClick={() => setModalOpen(true)}
className="btn btn-primary w-full"
>
Add new task
<AiOutlinePlus className="ml-2" size={18} />
</button>
<Modal modalOpen={modalOpen} setModalOpen={setModalOpen}>
<form onSubmit={handleSubmitNewTodo}>
<h3 className="font-bold text-lg">Add new task</h3>
<div className="modal-action">
<input
value={newTaskValue}
onChange={(e) => setNewTaskValue(e.target.value)}
type="text"
placeholder="Type here"
className="input input-bordered w-full"
/>
<button type="submit" className=" btn">
Submit
</button>
</div>
</form>
</Modal>
</div>
);
};
AddTask.tsxにaddTodo
を使ってタスクを追加します!
refreshしないと追加されたタスクが見えないのでuseRouter()でrefreshします。
またidをランダムで与えるため、uuidを使いました!
npm i uuid
npm i --save-dev @types/uuid
一番目のnpmで設置するときuuidからエラーが出るので
二番目のnpmで再設置します!
edit todolist(タスク修正)
addTodoをコビーしてeditができるAPIを作ります!
addTodoと同じほぼ感じでmethodだけPUTに変えます!
export const editTodo = async (todo: ITask): Promise<ITask> => {
const res = await fetch(`${baseUrl}/tasks/${todo.id}`, {
method: "PUT",
headers: {
"Content-type": "application/json",
},
body: JSON.stringify(todo),
});
const updatedTodo = await res.json();
return updatedTodo;
};
FormをHandlerするのでtype設定はFormEventHandler<HTMLFormElement>
を書きます。
inputにvalueをtaskToEditを与えてtask.textをHandlerします。
const router = useRouter();
const [openModalEdit, setOpenModalEdit] = useState<boolean>(false);
const [taskToEdit, setTaskToEdit] = useState<string>(task.text);
const handleSubmitEditTodo: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault();
await editTodo({
id: task.id,
text: taskToEdit,
});
setTaskToEdit("");
setOpenModalEdit(false);
router.refresh();
};
...
<FiEdit
onClick={() => setOpenModalEdit(true)}
cursor="pointer"
className="text-blue-500"
size={25}
/>
<Modal modalOpen={openModalEdit} setModalOpen={setOpenModalEdit}>
<form onSubmit={handleSubmitEditTodo}>
<h3 className="font-bold text-lg">Edit Task</h3>
<div className="modal-action">
<input
value={taskToEdit}
onChange={(e) => setTaskToEdit(e.target.value)}
type="text"
placeholder="Type here"
className="input input-bordered w-full"
/>
<button type="submit" className=" btn">
Submit
</button>
</div>
</form>
</Modal>
delete todolist(タスク削除)
deleteは簡単です!apiにdeleteを追加します。
export const deleteTodo = async (id: string): Promise<void> => {
await fetch(`${baseUrl}/tasks/${id}`, {
method: "DELETE",
});
};
const [openModalDeleted, setOpenModalDeleted] = useState<boolean>(false);
const handleDeleteTask = async (id: string) => {
await deleteTodo(id);
setOpenModalDeleted(false);
router.refresh();
};
<FiTrash2
onClick={() => setOpenModalDeleted(true)}
cursor="pointer"
className="text-red-500"
size={25}
/>
<Modal modalOpen={openModalDeleted} setModalOpen={setOpenModalDeleted}>
<h3 className="text-lg">
Are you sure, you want to delete this task?
</h3>
<div className="modal-action">
<button onClick={() => handleDeleteTask(task.id)} className="btn">
yes
</button>
</div>
</Modal>
感想
①すべてをモーダルコンポーネントの中でするのではなく、childrenをめくって使う方法もあることを知った
②React.FCはコンポーネントのタイプを定義してくれる
③formにタイプはForm Event Handler<HTML Form Element>
を使用する
Discussion