Next.js To Do App Tutorial
Learn how to create a basic To Do App with Next.js by example
Step 1: Create Next.js App
npx create-next-app@latest
Note: Opt out of TailwindCSS for this tutorial.
Step 2: Install dependencies
Install lodash for its debounce utility.
npm i lodash
npm i --save-dev @types/lodash
Step 3: Clear out the default styles
Remove everything in /app/globals.css
and /app/page.module.css
.
Step 4: Add the To Do App code to the home page
/app/page.tsx
"use client";
import { useEffect, useState } from "react";
import _ from "lodash";
interface Todo {
id: number;
title: string;
completed: boolean;
}
const URL = "https://jsonplaceholder.typicode.com";
export default function Home() {
const [todos, setTodos] = useState<Todo[]>();
useEffect(() => {
async function fetchData() {
const todos = await fetchTodos();
setTodos(todos);
}
fetchData();
}, []);
async function fetchTodos(): Promise<Todo[]> {
const response = await fetch(`${URL}/todos`);
return await response.json();
}
async function handleClick(todo: Todo) {
const idx = todos!.findIndex((t) => t.id === todo.id);
const newState = [...todos!];
newState.splice(idx, 1, { ...todo, completed: !todo.completed });
setTodos(newState);
const response = await fetch(`${URL}/todos/${todo.id}`, {
method: "PATCH",
body: JSON.stringify({
completed: !todo.completed,
}),
});
return await response.json();
}
async function handleCreate() {
const response = await fetch(`${URL}/todos`, {
method: "POST",
body: JSON.stringify({
title: "",
}),
});
const json = await response.json();
const newTodo: Todo = {
id: json.id,
title: "",
completed: false,
};
const newState = [newTodo, ...todos!];
setTodos(newState);
}
const debouncedTitleChange = _.debounce(
async (newTitle: string, todo: Todo) => {
await fetch(`${URL}/todos/${todo.id}`, {
method: "PATCH",
body: JSON.stringify({
title: newTitle,
}),
});
const idx = todos!.findIndex((t) => t.id === todo.id);
const newState = [...todos!];
newState.splice(idx, 1, { ...todo, title: newTitle });
setTodos(newState);
},
500
);
async function handleTitleChange(
e: React.ChangeEvent<HTMLInputElement>,
todo: Todo
) {
debouncedTitleChange(e.target.value, todo);
}
async function handleDelete(todo: Todo) {
const idx = todos!.findIndex((t) => t.id === todo.id);
const newState = [...todos!];
newState.splice(idx, 1);
setTodos(newState);
await fetch(`${URL}/todos/${todo.id}`, {
method: "DELETE",
});
}
async function addTenThousandTodos() {
const newTodos: Todo[] = [];
for (let i = 0; i < 10000; i++) {
newTodos.push({ id: 1000 + i, title: `to do ${i}`, completed: false });
}
setTodos([...newTodos, ...todos!]);
}
if (!todos) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Next.js TODO</h1>
{todos && (
<div>
<button onClick={handleCreate}>Create New To Do</button>
<button onClick={addTenThousandTodos}>Add 10,000 To Dos</button>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleClick(todo)}
/>{" "}
<input
type="text"
defaultValue={todo.title}
onChange={(e) => handleTitleChange(e, todo)}
/>
<button onClick={() => handleDelete(todo)}>delete</button>
</li>
))}
</ul>
</div>
)}
</div>
);
}
Notes
- "use client" is used at the top to make this page a client component, as there are user interactions on this page.
- jsonplaceholder is used as the fake API for our To Do App.
useState
is used for managing the state of the todos array.useEffect
is used to trigger thefetchTodos
function once.handleClick
is an async function that updates the state of the todos state and sends a PATCH request to the fake API.handleCreate
is an async function that sends a POST request to the fake API and adds the new Todo object to the todos state.debouncedTitleChange
is an async function wrapped by lodash's debounce to update the title of a todo. This is to limit the number of requests sent to the server.handleTitleChange
is an async function triggered by updating a Todo's title. Parameters are passed to the debounced function.addTenThousandToDos
is a function that we will use in an upcoming benchmarking test.- A loading div is returned if todos are not yet fetched and loaded.
- A JavaScript expression is used to map the list of todos to HTML elements.