Introduction
Check Finall Version
Setting up the Frontend
To save time and get into the core purpose of this tutorial, we’ll use a simple starter with basic functionality and components built-in. This starter comes with TailwindCSS and the heroicons library. We’ll explore the starter project structure, functionalities, and components before integrating with NextAuth, Prisma, and MongoDB. On your machine, clone the starter repo, and install dependencies.
https://github.com/haani0090/nextjs-crud-starter.gitcd into the directory# install dependenciesnpm install
Once installed, our directory structure should look something like this:
📦next-notes-app┣ 📂node_modules┣ 📂components┃ ┣ 📜Editor.js┃ ┣ 📜NotesList.js┃ ┗ 📜SiteHeader.js┣ 📂layouts┃ ┗ 📜default.js┣ 📂modules┃ ┣ 📜AppContext.js┃ ┗ 📜RandomID.js┣ 📂pages┃ ┣ 📂api┃ ┃ ┗ 📜hello.js┃ ┣ 📂[user]┃ ┃ ┗ 📜index.js┃ ┣ 📜index.js┃ ┗ 📜_app.js┣ 📂public┃ ┣ 📜favicon.ico┃ ┗ 📜vercel.svg┣ 📂styles┃ ┣ 📜Editor.css┃ ┣ 📜globals.css┃ ┣ 📜Home.module.css┃ ┣ 📜NoteList.css┃ ┗ 📜SiteHeader.css┣ 📜.eslintrc.json┣ 📜.gitignore┣ 📜next.config.js┣ 📜package.json┣ 📜postcss.config.js┣ 📜README.md┣ 📜tailwind.config.js┗ 📜yarn.lock
To start the application, run yarn dev and the app should start running at Localost.
You can view the starter example . Right now, we can create, update and delete notes. However, it’s all on the client side. When we refresh the page, we lose all the data because the state is managed in our app using the Context API
State management with Context API
Context API is a state management tool bundled with the React.js library itself. We typically need to pass data between components in any application. For example, to pass state from a parent component down to a child component, we would use props. However, passing state between sibling components and from a child component to its parent can become complex. That’s where state management comes in, allowing any component anywhere in the application to have access to the application state. We could use Redux or Recoil, but we’ll use the Context API. To see how our Context API works, open the ./modules/AppContext.js file. We won’t go in-depth about how Context API works in a Next.js application
// ./modules/AppContext.jsconst { createContext, useState, useContext, useReducer } = require("react");// context data getterconst NoteStateContext = createContext();const NotesStateContext = createContext();// context data setterconst NoteDispatchContext = createContext();const NotesDispatchContext = createContext();// reducer function to modify state based on action typesconst notesReducer = (state, action) => {const { note, type } = action;if (type === "add") {...}if (type === "remove") {...}if (type === "edit") {...}return state;};// NoteProvider, which will wrap the application// providing all the nested state and dispatch contextexport const NoteProvider = ({ children }) => {// useState for note, to get and set a single noteconst [note, setNote] = useState({});// use Reducer for notes, to get all notes// and add, edit or remove a note from the arrayconst [notes, setNotes] = useReducer(notesReducer, []);return (<NoteDispatchContext.Provider value={setNote}><NoteStateContext.Provider value={note}><NotesDispatchContext.Provider value={setNotes}><NotesStateContext.Provider value={notes}>{children}</NotesStateContext.Provider></NotesDispatchContext.Provider></NoteStateContext.Provider></NoteDispatchContext.Provider>);};// export state contextsexport const useDispatchNote = () => useContext(NoteDispatchContext);export const useNote = () => useContext(NoteStateContext);export const useDispatchNotes = () => useContext(NotesDispatchContext);export const useNotes = () => useContext(NotesStateContext);
Here, we have two basic types of context: the state context and the dispatch context created using the createContext hook. The state context will act as a data getter, while the dispatch context will act as a setter. To achieve this, within the NoteProvider function which wraps the children prop around the DispatchContext & StateContext providers, we use useState and useReducer hooks to create notes and setNotes for example.
const [notes, setNotes] = useReducer(notesReducer, []);
setNotes uses the useReducer hook and the notesReducer function to add, edit or delete a note from the notes array based on the action.type passed to the setNotes function and are passed into their context providers:
<NotesDispatchContext.Provider value={setNotes}><NotesStateContext.Provider value={notes}>
At the end of the file, the context is exported using the useContext hook:
export const useDispatchNotes = () => useContext(NotesDispatchContext);export const useNotes = () => useContext(NotesStateContext);
For the entire application to have access to the context, we need to include it in our ./pages/app.js file.
// ./pages/app.jsimport { NoteProvider } from "../modules/AppContext";import DefaultLayout from "../layouts/default";function MyApp({ Component, pageProps }) {return (<NoteProvider><DefaultLayout><Component {...pageProps} /></DefaultLayout></NoteProvider>);}export default MyApp;
Here, we wrap our application around the NoteProvider which we imported into the file
import { NoteProvider } from "../modules/AppContext";
This makes any state within NoteProvider available in all components in the application.
The NoteList component
To access and modify this global state, we import the context we need from ./modules/AppContext.js first.
// ./components/NotesList.jsimport { PencilAltIcon, TrashIcon, ExternalLinkIcon } from "@heroicons/react/solid";import { useNote, useDispatchNote, useNotes, useDispatchNotes } from "../modules/AppContext";const NotesList = ({ showEditor }) => {// this is where we assign the context to constants// which we will use to read and modify our global stateconst notes = useNotes();const setNotes = useDispatchNotes();const currentNote = useNote();const setCurrentNote = useDispatchNote();// function to edit note by setting it to the currentNote state// and adding the "edit" action// which will then be read by the <Editor /> componentconst editNote = (note) => {note.action = "edit";setCurrentNote(note);};// function to delete note by using the setNotes Dispatch notes functionconst deleteNote = (note) => {let confirmDelete = confirm("Do you really want to delete this note?");confirmDelete ? setNotes({ note, type: "remove" }) : null;};return (<div className="notes">{notes.length > 0 ? (<ul className="note-list">{notes.map((note) => (<li key={note.id} className="note-item"><article className="note"><header className="note-header"><h2 className="text-2xl">{note.title}</h2></header><main className=" px-4"><p className="">{note.body}</p></main><footer className="note-footer"><ul className="options"><li onClick={() => editNote(note)} className="option"><button className="cta cta-w-icon"><PencilAltIcon className="icon" /><span className="">Edit</span></button></li><li className="option"><button className="cta cta-w-icon"><ExternalLinkIcon className="icon" /><span className="">Open</span></button></li><li className="option"><button onClick={() => deleteNote(note)} className="cta cta-w-icon"><TrashIcon className="icon" /><span className="">Delete</span></button></li></ul></footer></article></li>))}</ul>) : (<div className="fallback-message"><p>Oops.. no notes yet</p></div>)}</div>);};export default NotesList;
The Editor component
The function of the Editor component is simple, it gets current note data from the application state by importing useNote() from ./modules/AppContext.js and assigning it to currentNote. It adds a new note to the global notes array or updates an existing note depending on the action type.
import { useEffect, useState } from "react";import { CheckCircleIcon } from "@heroicons/react/solid";import { useNote, useDispatchNote, useNotes, useDispatchNotes } from "../modules/AppContext";import RandomID from "../modules/RandomID";const Editor = () => {// the current noteconst currentNote = useNote();const setCurrentNote = useDispatchNote();// the array of saved notesconst notes = useNotes();const setNotes = useDispatchNotes();// editor note statesconst [title, setTitle] = useState("Hola");const [body, setBody] = useState(`There once was a ship that put to seaand the name of the ship was the billy old tea`);const [noteID, setNoteID] = useState(null);const [noteAction, setNoteAction] = useState("add");const [isSaved, setIsSaved] = useState(false);// function to update textarea content and heightconst updateField = (e) => {// get textarealet field = e.target;//set body state to textarea valuesetBody(field.value);// reset textarea heightfield.style.height = "inherit";// Get the computed styles for the textarealet computed = window?.getComputedStyle(field);// calculate the heightlet height =parseInt(computed.getPropertyValue("border-top-width"), 10) +parseInt(computed.getPropertyValue("padding-top"), 10) +field.scrollHeight +parseInt(computed.getPropertyValue("padding-bottom"), 10) +parseInt(computed.getPropertyValue("border-bottom-width"), 10);// set the new heightfield.style.height = `${height}px`;};// function to save note to saved notes arrayconst saveNote = () => {// check if the title input & body textarea actually contain textif (title && body) {// check if note already has an ID, if it does asign the current id to the note object,// if not, assign a new random ID to the note objectlet id = noteID || RandomID(title.slice(0, 5), 5);// the note objectlet note = {id,title,body,};try {if (noteAction == "edit") {// edit in notes listsetNotes({ note, type: "edit" });console.log({ note, noteAction, noteID, notes });} else {// add to notes listsetNotes({ note, type: "add" });}// change isSaved state to true, thereby disabling the save buttonsetIsSaved(true);// clear note contentnote = { title: "", body: "" };// clear editorsetTitle(note.title);setBody(note.body);// clear current note statesetCurrentNote(note);} catch (error) {console.log({ error });}}};// enable the button whenever the content of title & body changesuseEffect(() => {if (title && body) setIsSaved(false);else setIsSaved(true);}, [title, body]);// update the editor content whenever the note context changes// this acts like a listener whenever the user clicks on edit note// since the edit note funtion, setsuseEffect(() => {if (currentNote.title && currentNote.body) {setTitle(currentNote.title);setBody(currentNote.body);setNoteID(currentNote.id);setNoteAction(currentNote.action);}}, [currentNote]);return (<div className={"editor"}><div className={"wrapper"}><div className="editing-area"><div className="title"><input value={title} onChange={(e) => setTitle(e.target.value)} type="text" className={"form-input"} placeholder="Title" /></div><div className="body"><textareavalue={body}onChange={(e) => updateField(e)}name="note-body"id="note-body"className="form-textarea"cols="10"rows="2"placeholder="Write something spec ✨"></textarea></div></div><ul className={"options"}><li className={"option"}><button onClick={saveNote} disabled={isSaved} className="cta flex gap-2 items-end"><CheckCircleIcon className="h-5 w-5 text-blue-500" /><span className="">{isSaved ? "Saved" : "Save"}</span></button></li></ul></div></div>);};export default Editor;
Hopefully, we now know the basics of how the app works. Let’s proceed to NextAuth, Prisma & MongoDB.
Setting up NextAuth, Prisma & MongoDB
An Adapter in NextAuth.js connects your application to whatever database or backend system you want to use to store data for users, their accounts, sessions, etc. We’ll be using the Prisma adapter. To use this Adapter, you need to install Prisma Client, Prisma CLI, and the separate @next-auth/prisma-adapter package:
npm i next-auth @prisma/client @next-auth/prisma-adapternpm i prisma --dev
Let’s set up our MongoDB cluster so we can configure our NextAuth.js to use the Prisma Adapter. Log in to MongoDB and set up a MongoDB cluster on Atlas Go to Projects and create a new project:
Next, we’ll add permissions to our project and be assigned Project Owner. Next, create a database for our project.
Once we click on Build a database, we’ll be asked to choose our database plan; you can use the free shared plan. Next, we select the providers and region for our database cluster; you can leave it as is and use the M0 cluster, which is free.
Click on Create Cluster. Next, we’ll have to set up our database security.
Choose username and password, create a new user, in the “Where would you like to connect from?” section, choose “My Local Environment” and Add My Current IP Address. Click on Finish. Next, we need to get our connection URL to connect to our cluster. Click on Connect your applicationCopy the connection string provided
Now that we’ve gotten or database connection string, add it to a .env file. With a few other environment variables required by NextAuth.
// .envDATABASE_URL=mongodb+srv://username:password@cluster0.ba9ic.mongodb.net/myFirstDatabase?retryWrites=true&w=majorityNEXTAUTH_SECRET=somesecretNEXTAUTH_URL=http://localhost:3000
🚨 Make sure you include the database name in your MongoDB URL. For example, the database used here is myFirstDatabase.
Working with Prisma
We can use the Prisma CLI tool to create a couple of Prisma-related files. Run:
npx prisma init
This creates a basic /prisma/schema.prisma file. This schema is adapted for use in Prisma and based on NextAuth main schema. We’ll modify the schema to work with NextAuth; enter the following code into the schema:
// ./prisma/schema.prismadatasource db {provider = "mongodb"url = env("DATABASE_URL")}generator client {provider = "prisma-client-js"}model Account {id String @id @default(auto()) @map("_id") @db.ObjectIduserId Stringtype Stringprovider StringproviderAccountId Stringrefresh_token String? @db.Stringaccess_token String? @db.Stringexpires_at Int?token_type String?scope String?id_token String? @db.Stringsession_state String?user User @relation(fields: [userId], references: [id], onDelete: Cascade)@@unique([provider, providerAccountId])}model Session {id String @id @default(auto()) @map("_id") @db.ObjectIdsessionToken String @uniqueuserId Stringexpires DateTimeuser User @relation(fields: [userId], references: [id], onDelete: Cascade)}model User {id String @id @default(auto()) @map("_id") @db.ObjectIdname String?email String? @uniqueemailVerified DateTime?image String?accounts Account[]sessions Session[]}model VerificationToken {identifier String @id @default(auto()) @map("_id") @db.ObjectIdtoken String @uniqueexpires DateTime@@unique([identifier, token])}
Now that we have written our schema, we can create the collections in our database. Using the Prisma using the CLI tool, run npx prisma db push, and you should see:
npx prisma generate
This command reads our Prisma schema and generates a version of Prisma Client that is tailored to our models.
We can start using PrismaClient to interact with our database. We’ll use a single PrismaClient instance that we can import into any file where needed. Create a new ./prisma/prisma.ts file:
// prisma/prisma.tsimport { PrismaClient } from '@prisma/client'let prismaif (process.env.NODE_ENV === 'production') {prisma = new PrismaClient()} else {if (!global.prisma) {global.prisma = new PrismaClient()}prisma = global.prisma}export default prisma
Now, let’s proceed to complete our NextAuth integration.
Set up our Google application
We’ll be using Google provider for this project, and so we need to get our environment variables to configure our Google provider. First, we log into Google Cloud console,
Click on the dropdown which opens a modal; in the top right corner of the modal, click on New Project, to create a new project.
Now, in our new app we can open the side bar, go to APIs & Services > Credentials. The first thing we need to do is to configure the consent screen: Then choose External User type. On the next screen, we add our app information.Then, we add the developer information and click on Save & Continue on this screen and the following screens until it’s complete, and we go back to the dashboard.
In the side menu, under API & CREDENTIALS, click on Credentials to create a new **OAuth Client ID.In the next screen, we’ll select the application type and name, and add an authorized Google URI for development -
Click on Create, and after the client ID has been created, we’ll have our Client ID and Client Secret. Create a .env file to save these variables
Disclaimer! don't show these Credentials to Anyone Else keep It Private. i'll Show You because after This I'm going to remove these From my
// .envGOOGLE_CLIENT_ID=<client_ID_here>GOOGLE_CLIENT_SECRET=<client_secret_here>DATABASE_URL=mongodb+srv://username:password@cluster0.ba9ic.mongodb.net/myFirstDatabase?retryWrites=true&w=majorityNEXTAUTH_SECRET=somesecretNEXTAUTH_URL=http://localhost:3000
Set up NextAuth with the Prisma adapter
To add NextAuth.js to a project, create a file called [...nextauth].js in pages/api/auth. This contains the dynamic route handler for NextAuth.js, which will also contain all your global NextAuth.js configurations. All requests to /api/auth/* (signIn, callback, signOut, etc.) will automatically be handled by NextAuth.js. We also configure your NextAuth.js to use the Prisma Adapter and Prisma client.
// ./pages/api/auth/[...nextauth].jsimport NextAuth from "next-auth"import GoogleProvider from "next-auth/providers/google"import { PrismaAdapter } from "@next-auth/prisma-adapter"import prisma from "../../../prisma/prisma";export default NextAuth({adapter: PrismaAdapter(prisma),providers: [GoogleProvider({clientId: process.env.GOOGLE_CLIENT_ID,clientSecret: process.env.GOOGLE_CLIENT_SECRET,}),],callbacks: {async session({ session, token, user }) {session.user.id = user.id;return session;},},});
Take note of the callbacks object; this is where we populate the user session with their id as NextAuth does not provide this value by default.
Configure the NextAuth Shared session state
The useSession() React Hook in the NextAuth.js client is the easiest way to check if someone is signed in. To be able to use useSession first you’ll need to expose the session context, link with title at the top level of your application:
// ./pages/_app.jsimport { SessionProvider } from "next-auth/react";import { NoteProvider } from "../modules/AppContext";import DefaultLayout from "../layouts/default";function MyApp({ Component, pageProps: { session, ...pageProps } }) {return (<SessionProvider session={session}><NoteProvider><DefaultLayout><Component {...pageProps} /></DefaultLayout></NoteProvider></SessionProvider>);}export default MyApp;
Instances of useSession will then have access to the session data and status. The link with titlealso keeps the session updated and synced between browser tabs and windows.
Add Login functionality
To add the login and logout actions, we’ll create a component ./components/AuthBtn.js, which will be placed in our ./components/SiteHeader.js component.
// ./components/AuthBtn.jsimport { ChevronDownIcon, RefreshIcon } from "@heroicons/react/solid";import { useSession, signIn, signOut } from "next-auth/react";import Image from "next/image";const AuthBtn = () => {const { data: session, status } = useSession();if (status === "loading") {return (<div className="auth-btn"><div className="auth-info"><RefreshIcon className="icon animate-spin" /></div></div>);}if (status === "unauthenticated") {return (<div className="auth-btn"><button onClick={() => signIn()}>Login</button></div>);}return (<div className="auth-btn"><div className="auth-info pr-2"><Image src={session.user.image} alt={session.user.name} width={30} height={30} className="rounded-full" /><p>Hi, {session.user.name}</p></div><div className="dropdown"><button className="dropdown-btn !py-1"><ChevronDownIcon className="icon" /></button><ul className="dropdown-list opacity-0 invisible"><li className="dropdown-item"><button onClick={() => signOut()} className="cta">Logout</button></li></ul></div></div>);};export default AuthBtn;
Here, we render our UI based on the state returned by status, which maps to three possible session states:
"loading" - A rotating spinning Icon "authenticated" - Username, picture and logout dropdown "unauthenticated" - A login button Quick note: To display images from another domain using Next.js Image , we need to add it to a list of domains in our ./next.config.js file
// next.config.jsconst nextConfig = {reactStrictMode: true,images: {domains: ['lh3.googleusercontent.com'],},}module.exports = nextConfig
Next, we update our Prisma schema with the Note model so that we can manage notes in our MongoDB database.
Add Note Model to Prisma Schema
In our ./prisma/schema.prisma file, we’re going to add Note model:
// ./prisma/schema.prismamodel Note {id String @id @default(auto()) @map("_id") @db.ObjectIdtitle Stringbody StringuserId String?user User? @relation(fields: [userId], references: [id], onDelete: Cascade)@@unique([id, userId])}
We’ll also have to add the Note reference to our User model:
// ./prisma/schema.prismamodel User {id String @id @default(auto()) @map("_id") @db.ObjectIdname String?email String? @uniqueemailVerified DateTime?image String?accounts Account[]sessions Session[]notes Note[] //add note reference}
Once again, to sync with our database, run npx prisma db push
We should see the new Note collection if we check our MongoDB database.
Add create new note functionality
Let’s create our CRUD functions. First, a createNote function will add a note to our database with Prisma. As we progress, we’ll create reading, updating, and deleting functions. Create a new file ./prisma/note.js which will contain all our CRUD functions:
// ./prisma/Note.jsimport prisma from "./prisma";// READ//get unique note by idexport const getNoteByID = async (id) => {const note = await prisma.note.findUnique({where: {id,},include: {user: true,},});return note;};// CREATEexport const createNote = async (title, body, session) => {const newNote = await prisma.note.create({data: {title,body,user: { connect: { email: session?.user?.email } },},});const note = await getNoteByID(newNote.id);return note;};
Here, our create function accepts a few parameters - title, body and session. session will contain the current session data, including information about the user, specifically user.email. We’re using prisma.note.create() to create a new note by passing an object with the data key, which is an object containing title, body, and user. Since the user field is relational, we’re using connect to connect the new note to an existing user with the email provided. Next, we must create an API endpoint to run this function.
Add Create Note API endpoint
Create a new file ./pages/api/note.js.
// pages/api/note.jsimport { createNote } from "../../prisma/Note";import { getSession } from "next-auth/react";export default async function handle(req, res) {// Get the current session data with {user, email, id}const session = await getSession({ req });// Run if the request was a post requestif (req.method == "POST") {// Get note title & body from the request bodyconst { title, body } = req.body;// Create a new note// also pass the session which would be use to get the user informationconst note = await createNote(title, body, session);// return created notereturn res.json(note);}}
Here, we’re creating an API endpoint. Whenever a request goes to /api/note, we handle it here. First, we check we get the authenticated user session with the getSession helper function from NextAuth.js. For a POST request, we get the title and body from the request. We then run the createNote function that we created earlier. Next, let’s send a create request from our frontend in the Editor component.
Send create request from Frontend
🚨 To ensure that only authenticated users can create notes, in the editor component, we must render only when the session status is “authenticated.”
// ./components/Editor.jsconst Editor = () => {// ..return (status === "authenticated" && (<div className={"editor"}> {/* ... */} </div>));};
In our ./components/Editor.js file, using the fetch API, we’ll send a POST request to our create endpoint /api/note and save the data to our Notes context state. Let’s edit our saveNote function.
// ./components/Editor.js// ...const saveNote = async () => {if (title && body) {// ...try {if (noteAction == "edit") {// ...} else {// send create request with note datalet res = await fetch("/api/note", {method: "POST",headers: { "Content-Type": "application/json" },body: JSON.stringify(note),});const newNote = await res.json();console.log("Create successful", { newNote });// add to notes list (global context state)setNotes({ note: newNote, type: "add" });}// ...}}}// ...
Add update note functionality
Similarly, we’ll create an update function. In our ./prisma/note.js file, create a new updateNote() function.
/ ./prisma/Note.js// ...// UPDATEexport const updateNote = async (id, updatedData, session) => {let userId = session?.user.id;const updatedNote = await prisma.note.update({where: {id_userId: {id,userId,},},data: {...updatedData,},});const note = await getNoteByID(updatedNote.id);return note;};
Here, we get the note id, updatedData, and user session. From the user session, we get the userId. In prisma.note.update, we filter the note to update by the userId and note id by combining the unique fields and concatenating them with an underscore.
where: {id_userId: {id,userId,},},},
And then pass the updatedData to data. Next, we’ll create the API endpoint to call our update function.
Add update note API endpoint
Back in our ./pages/api/note.js file, for PUT requests, we’ll get and pass the note id, title , body and session and pass it into our updateNote() function.
// ./pages/api/note.js// ...export default async function handle(req, res) {// Run if the request is a POST request// ...// Run if the request is a PUT requestelse if (req.method == "PUT") {const { id, title, body } = req.body;// const updatedData = {title, body}// Update current note// also pass the session which would be use to get the user informationconst note = await updateNote(id, { title, body }, session);// return updated notereturn res.json(note);}}
Now, back in our ./components/Editor.js component, let’s add the request for a note update.
// ./components/Editor.js// ...const saveNote = async () => {if (title && body) {// ...try {if (noteAction == "edit") {// add note id to note datanote.id = noteID;// send request to edit notelet res = await fetch("/api/note", {method: "PUT",headers: { "Content-Type": "application/json" },body: JSON.stringify(note),});// update noteconst updatedNote = await res.json();console.log("Update successful", { updatedNote });// edit in notes listsetNotes({ note: updatedNote, type: "edit" });} else {// send create request with note data// ...}// ...}}}// ...
Here, we get the note id from the noteID state variable. In our fetch function, we send a PUT request with the note data and save the response to our Notes context state. Let’s see it in action:
So far, we’ve implemented Create and Update functionality to our application, but if we still refresh, we see that our notes disappear since we can’t yet get notes from the database. Next, we’ll have to make some changes to be able to load notes from our database.
Update view to display notes from the database
First, we’ll create more helper functions to get all notes from the authenticated user.
// ./prisma/Note.js// ...// get notes by userexport const getAllNotesByUserID = async (id) => {const notes = await prisma.note.findMany({where: {userId: id,},include: {user: true,},});return notes;};
Next, on our home page ./pages/index.js, we’ll get the notes on the server-side using getServerSideProps
// ./pages/index.jsimport { getSession } from "next-auth/react";const getAllNotesByUserID = require("../prisma/Note").getAllNotesByUserID;// ...export const getServerSideProps = async ({ req, res }) => {const session = await getSession({ req });if (!session) {res.statusCode = 403;return { props: { notes: [] } };}const notes = await getAllNotesByUserID(session?.user?.id);return {props: { notes },};};const Home = ({ notes }) => {const [showEditor, setShowEditor] = useState(true);return (<>{/* ... */}<div className={HomeStyles.container}><main className={HomeStyles.main}><div className="wrapper m-auto max-w-8xl">{/* ... */}{/* Note list component */}<NotesList retrieved_notes={notes} /></div></main></div></>);};
🚨 Here, we’re using require to import our getAllNotesByUserID helper function to avoid Next.js trying to run it on the client-side. Then, in the ./components/NoteList.js component, we get retrieved_notes as props, and in a useEffect hook, replace the application notes state with the fetched notes:
// ./components/NoteList.jsimport { useEffect } from "react";import Image from "next/image";// ...const NotesList = ({ retrieved_notes, showEditor }) => {// ...useEffect(() => {// replace notes in notes context statesetNotes({ note: retrieved_notes, type: "replace" });}, [retrieved_notes]);return (<div className="notes">{notes.length > 0 ? (<ul className="note-list">{notes.map((note) => (<li key={note.id} className="note-item">{/* ... */}<footer className="note-footer"><ul className="options"><li className="option">{/* add user image to note footer */}<Image src={note.user.image} alt={note.user.name} width={32} height={32} className="rounded-full" /></li>{/* ... */}</ul></footer></article></li>))}</ul>) : ({/* ... */})}</div>);};
Also, in our .modules/AppContext.js file, we add support for the replace action type in our reducer function.
const notesReducer = (state, action) => {// get the note object and the type of action by destructuringconst { note, type } = action;// if "replace"// replace the entire array with new valueif (type === "replace") return note;// ...}
const notesReducer = (state, action) => {// get the note object and the type of action by destructuringconst { note, type } = action;// if "replace"// replace the entire array with new valueif (type === "replace") return note;// ...}
Awesome.We can now get our notes from the database and update the application state when the page loads.
Add delete note functionality
To delete a note, we need to create a delete helper function. In our ./prisma/Note.js file, add this function:
// ./prisma/Note.js// ...// DELETEexport const deleteNote = async (id, session) => {let userId = session?.user.id;const deletedNote = await prisma.note.delete({where: {id_userId: {id,userId,},},});return deletedNote;};
Next, create a delete handler in our ./pages/api/note.js file.
// pages/api/note.jsexport default async function handle(req, res) {if (req.method == "POST") {// ... Create a new note}else if (req.method == "PUT") {// ... Update current note}// Run if the request is a DELETE requestelse if (req.method == "DELETE") {const { id } = req.body;const note = await deleteNote(id, session);// return deleted notereturn res.json(note);}}
And finally, in our ./components/NoteList.js component, we’ll send a DELETE request in our deleteNote function.
// ./components/NoteList.jsconst NotesList = ({ retrieved_notes, showEditor }) => {// ...// function to delete note by using the setNotes Dispatch notes functionconst deleteNote = async (note) => {let confirmDelete = confirm("Do you really want to delete this note?");try {let res = await fetch(`/api/note`, {method: "DELETE",headers: { "Content-Type": "application/json" },body: JSON.stringify(note),});const deletedNote = await res.json();confirmDelete ? setNotes({ note: deletedNote, type: "remove" }) : null;} catch (error) {console.log(error);}};// ...}
Awesome! Finally, we can create dynamic routes to display individual notes. Create a new dynamic page ./pages/note/[id].js
// ./pages/note/[id].jsimport Head from "next/head";import Image from "next/image";import { getSession } from "next-auth/react";const getNoteByID = require("../../prisma/Note").getNoteByID;import HomeStyles from "../../styles/Home.module.css";export const getServerSideProps = async ({ req, res, params }) => {const session = await getSession({ req });console.log({ params });const { id } = params;if (!session) {res.statusCode = 403;return { props: { note: null } };}const note = await getNoteByID(id);return {props: { note },};};const Note = ({ note }) => {if (note == null) {return (<><Head><title>Login to view note</title><meta name="description" content="Login to view this note" /><link rel="icon" href="/favicon.ico" /></Head><div className={HomeStyles.container}><main className={HomeStyles.main}><header className="max-w-4xl mt-24 mx-auto"><h1 className="text-4xl">Oops... You have to login to view this note</h1></header></main></div></>);}return (<><Head><title>{note.title}</title><meta name="description" content={`By ${note.user.name}`} /><link rel="icon" href="/favicon.ico" /></Head><div className={HomeStyles.container}><main className={HomeStyles.main}><article className="note max-w-4xl m-auto mt-20"><header className="note-header"><h2 className="text-4xl">{note.title}</h2></header><main className=" px-4"><p className="text-xl">{note.body}</p></main><footer className="note-footer"><ul className="options px-4"><li className="option">{/* add user image to note footer */}<Image src={note.user.image} alt={note.user.name} width={48} height={48} className="rounded-full" /></li></ul></footer></article></main></div></>);};export default Note;
Next, in the ./components/NoteList.js component, we’ll use Next.js Link component to wrap our “open” button to route each note page by its id.
// ./components/NoteList.js//...<li className="option"><Link href={`/note/${note.id}`} target={`_blank`} rel={`noopener`}><button className="cta cta-w-icon"><ExternalLinkIcon className="icon" /><span className="">Open</span></button></Link></li>another word//...
Now, if we click the open button, we’ll be taken to the note page.
We’re done! Awesome!
Conclusion
It’s a popular fact that Next.js and Prisma are great for building fast full-stack applications. In this tutorial, we built a notes application where users can log in to the application thanks to NextAuth, create, view, edit and delete notes. All user information and notes are saved to a MongoDB database. To interact with the database from the Next.js application, we used the Prisma ORM, which helps us define our schema and create and connect to our database for us to manage it.
Further reading & resources
Here are some excellent links and resources to help you as you explore Next.js & Prisma