📈

Next.jsでTodoリストを作った!

2023/11/05に公開

https://simple-todoapp-sunnyheee-sunnyheees-projects.vercel.app/?_vercel_share=FpBHdb5AbQmLqlj42NJuKUzMdchBDAvc

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で作ります!

AddTask.tsx
    <div>
      <button className="btn btn-primary w-full">
        Add new task
        <AiOutlinePlus className="ml-2" size={18} />
      </button>
    </div>

リストを追加したらデーターが入るtableを作ります。
daisyuiでコビーしてもってきました!

TodoList.tsx
<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
package.json
json-server --watch data/todos.json --port 9000
api.ts
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を設定して型を与えます。

type.ts
export interface ITask {
  id: string;
  text: string;
}
  const tasks = await getAllTodos();
  console.log(tasks);

consoleで確認してみます!

type設定

TodoListにap情報を渡すときtypeのエラーが発生します。
componentの内部でもtypeを設定したらエラーが消されます!

page.tsx
 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をもってきます

TodoList.tsx
interface TodoListProps {
  tasks: ITask[];
}

const TodoList: React.FC<TodoListProps> = () => {
...
}

Task component化

TodoList.tsxのtask部分をコンポネート化しましょう!

TodoList.tsx
<tbody>
  {tasks.map((task) => (
    <Task key={task.id} task={task} />
  ))}
</tbody>
Task.tsx
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を消します!

daisyuiでmadalをもってきます!
https://daisyui.com/components/modal/

タスクを追加したいときmodalから追加することにしたいのでAddTaskにModalコンポーネントを追加します!

AddTask.tsx
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>
  );
Modal.tsx
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(タスク追加)

リストを追加するためaddTodoAPIを作ります。

api.ts
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();
};
AddTask.tsx
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に変えます!

api.td
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を追加します。

api.ts
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