[T3 Stack] Todo App: Part 2 — CRUD Operations

Wormdirt Development
4 min readOct 13, 2023

--

Next on the docket is adding some more functionality to our tRPC server so we can ADD a todo, DELETE a todo, and even change it’s status between done and not done.

Login and Logout

Before we move onto the tRPC stuff, lets make sure we can log in and log out of our application — we will need the user session in a minute. This will also be the start of our first component. We will be separating out our code into many components down the line. For now let’s start simple:

src/components/header.tsx

import { signIn, signOut, useSession } from "next-auth/react";
import Link from "next/link";

export const Header = () => {
const { data: session } = useSession();

return (
<div className="flex justify-between px-2 py-4 border-b">
<div>TODO APP</div>

<div>
{ session?.user ? (
<div className="flex flex-row gap-2">
<p>{ session.user.name }</p>
<button onClick={() => void signOut()}>Sign Out</button>
</div>
) : (
<button onClick={() => void signIn()}>Sign In</button>
)}
</div>
</div>
)
}

It’s not going to be the prettiest navigation bar right now, but it will serve it’s purpose. If you are logged in it will show your name and the logout button. If you are not logged in, it will show you the login button which will redirect you to Google auth page.

Updating our Router

Inside the todo router file, I create a few objects that define Zod(link) objects for TypeScript validation. We use these objects in our mutations:

src/server/api/routers/todos.ts

import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";


/**
* ZOD OBJECTS
* addTodoInput - ensures we get the userId, titles, details, done data
* setDoneInput - ensures we get the todo's id, and done properties
*/
const addTodoInput = z.object({
userId: z.string(),
title: z.string(),
details: z.string(),
done: z.boolean(),
});

const setDoneInput = z.object({
id: z.string(),
done: z.boolean(),
})

export const todoRouter = createTRPCRouter({
/**
* NEW TRPC FUNCTIONS
* We changed the getAll function to get getTodosByUser
* Then we added the ability to create a todo,
* delete a todo, and change the done state of a todo
*/
getTodosByUser: publicProcedure.input(z.string()).query(async ({ ctx, input }) => {
const todos = await ctx.db.todo.findMany({
where: {
userId: input
}
})
return todos
}),

createTodo: publicProcedure.input(addTodoInput).mutation(async ({ ctx, input }) => {
const todo = await ctx.db.todo.create({
data: {
userId: input.userId,
title: input.title,
details: input.details,
done: input.done
}
})
return todo
}),

deleteTodo: publicProcedure.input(z.string()).mutation(async ({ ctx, input}) => {
return await ctx.db.todo.delete({
where: {
id: input
}
})
}),

setDone: publicProcedure.input(setDoneInput).mutation(async ({ ctx, input }) => {
await ctx.db.todo.update({
where: {
id: input.id
},
data: {
done: input.done
}
})
})

});

Wiring up the Frontend

Now that we have those functions in place, lets start using them in our frontend so we can see the magic of tRPC. The first logical step to implement is the ability to add a todo.

src/pages/index.tsx

import { useSession } from 'next-auth/react';
import React, { useState } from 'react
import { api } from "~/utils/api";

export default function Home() {
const { data: session } = useSession();
const [title, setTitle] = useState("");
const [details, setDetails] = useState("");

const ctx = api.useContext();

const { data, isLoading: todosLoading } =
api.todo.getTodosByUser.useQuery(session?.user?.id ?? "");

const { mutate } = api.todo.createTodo.useMutation({
onSuccess: () => {
setTitle("");
setDetails("");
void ctx.todo.getTodosByUser.invalidate()
}
})

return (
<div className="flex grow flex-col">
{ data?.map((todo) => (
<div>{ todo.title }</div>
))}

/**
* We will add a simple form for now so we can add a todo item
*/
<div>
<input
type="text"
placeholder="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<textarea
placeholder="Details"
value={details}
onChange={(e) => setDetails(e.target.value)}
/>
<button
onClick={() => mutate({
userId: session?.user.id ?? "",
title: title,
details: details,
done: false
})}
>Add Todo</button>
</div>
</div>
);
}

In the above code, we are grabbing the user from the auth session, creating variables for our form state (title and detail), grabbing the context (ctx) from our api, and then using getTodosByUser and createTodo from our tRPC api calls.

We extracted out the data and loading state (to be used later) from our getTodosByUser function, and then we extracted out the mutate ability from our createTodo function. The onSuccess call allows us to refresh the page once the data has successfully been submitted.

Set Done and Delete

Next, we can throw in the functionality to set the todo as done, and also delete the todo. For now, we won’t focus on styling and just add a checkbox and delete button, then test to see if thing work.

src/pages/index.tsx

// Previous code
export default function Home() {
// previous code...

const { mutate: setDoneMutate } = api.todo.setDone.useMutation({
onSuccess: () => {
void ctx.todo.getTodosByUser.invalidate();
}
})
const { mutate: deleteMutate } = api.todo.deleteTodo.useMutation({
onSuccess: () => {
void ctx.todo.getTodosByUser.invalidate();
}
})

return (
{ data?.map((todo) => (
<div>
<div className="flex gap-2">
<input
type="checkbox"
style={{ zoom: 1.5 }}
checked={!!todo.done}
onChange={() => {
setDoneMutate({
id: todo.id,
done: todo.done ? false : true
})
}}
/>
<p>{ todo.title }</p>
<button
onClick={() => deleteMutate(todo.id)}
>
Delete
</button>
</div>
))}

// todo form below
)
}

Testing it Out

Now let’s test things out and see if we have it working properly! Make sure your server is running and add some todos! First thing is first, you should see them appear on the webpage. Next, we can check Prisma Studio to look at our data!

npx prisma studio

Environment variables loaded from .env
Prisma schema loaded from prisma\schema.prisma
Prisma Studio is up on http://localhost:5555
If all goes right, you should see something like this.

Coming Up Next

In part 3 of this series, we will start cleaning up the code, separating out things into their own components, and adding some styling using ShadCN.

--

--