Next.js MUI Comments Section with Basic Authentication

Table of Contents
Overview
A comments section is critical to any dynamic website, as it allows users to engage with the content and provide feedback. In this blog, we will explore the process of building a comments section in a Next.js project. We will cover how to build a simple and user-friendly comments section. Whether a beginner or an experienced web developer, this guide will provide the knowledge along with a practical example to help you create a comments section for your Next.js project. So, let’s dive in and start building!
Source Files
GitHubPrerequisites
If you are not using the source files above, get the Next.js template from MUI. You might have to clone the entire repository to get it, but it is worth it. The template from MUI has everything configured out of the box and is ready to use. Installing Material UI yourself can be challenging unless you are already comfortable with Next.js and Material UI.
https://github.com/mui/material-ui/tree/master/examples/nextjs
After cloning or downloading the template, go into the directory and run
npm install
Additional Libraries Used
If you want to do this from scratch. I recommend cloning the source files in case the library versions have changed. Plus the example application in GitHub has a few extra pages. For more information on how to create a new Next.js app click here.
Axios is used for client side data operations but not necessary if you would rather use fetch.
MomentJS for date functions. Example – Post 3 Days ago

SQLite and SQLite3 for database connectivity.
React Infinite Scroll Component for loading records as you scroll down.
npm install axios moment sqlite sqlite3 react-infinite-scroll-component
Next-Auth
Next Auth is used to authenticate and secure pages. There are two hardcoded users in the example application.
npm install next-auth
Configure Next.js with tsconfig.json

To add aliases for paths and other framework configurations, add a file labeled ‘.\tsconfig.json’ to the root of your application.
The configuration below lets you use ‘@\components\filename.js’ instead of ‘..\..\..\components\filename.js’.
JSON source for tsconfig.json. For JavaScript only with no TypeScript label the file jsconfig.json.
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/components/*": ["components/*"],
"@/src/*": ["src/*"],
"@/lib/*": ["lib/*"],
"@/styles/*": ["styles/*"],
},
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}
Running Next.js App
Make sure you are in the root of the application when you run it.
npm run dev
SQLITE
There are two tables for this example. A Users and Comments table. Please use the database file labeled ‘comments.db‘ from GitHub. Get the SQLite Studio desktop application here.

Comments Table
Set the UserId column as the primary key with auto increment.
CREATE TABLE Users (
UserId INTEGER PRIMARY KEY,
Name TEXT,
Email TEXT,
Image TEXT,
DateJoined DATETIME
);
Comment Replies Table
Set the CommentId column as the primary key with auto increment.
CREATE TABLE Comments (
CommentId INTEGER PRIMARY KEY,
UserId INTEGER NOT NULL,
Content TEXT,
IsActive TINYINT,
DateCreated DATETIME,
DateModified DATETIME
);
Building the API
The API is built locally in NextJS using Next-Auth to handle authentication
Next-Auth

There are two users hardcoded using the CredentialsProvider provider.
Code for “pages\api\auth\[…nextauth].ts“. Note: […nextauth].ts must be in the folder structure ”.\pages\auth\[…nextauth].ts” in order for it to work.
There are two hard coded users named Jennifer Smith and Terry Oscar. Both users have a unique avatar picture and user id.
import NextAuth, { NextAuthOptions } from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
id: "domain-login",
name: "Jennifer Smith",
credentials: {
username: { label: "Username", type: "text", placeholder: "jsmith" },
password: { label: "Password", type: "password", }
},
async authorize(credentials, req) {
const user = { id: 1, name: "Jennifer Smith", email: "jennifers@example.com", image: 'https://randomuser.me/api/portraits/women/72.jpg' }
if (user) {
return user
} else {
return null
}
}
}),
CredentialsProvider({
id: "user-login",
name: "Terry Oscar",
credentials: {
username: { label: "Username", type: "text", placeholder: "jsmith" },
password: { label: "Password", type: "password" }
},
async authorize(credentials, req) {
const user = { id: 2, name: "Terry Oscar", email: "terryo@example.com", image: 'https://randomuser.me/api/portraits/women/67.jpg' }
if (user) {
return user
} else {
return null
}
}
}),
],
pages: {
signIn: '/login',
signOut: '/auth/signout',
},
theme: {
colorScheme: "light",
},
callbacks: {
async jwt({ token }) {
token.userRole = "admin"
return token
},
},
}
export default NextAuth(authOptions)
Enable Custom Sign In and Sign Out Page
There is a custom Sign In and Sign Out pages enabled pages in the […nextauth].ts file below.
...
export const authOptions: NextAuthOptions = {
providers: [
...
],
pages: {
signIn: '/login',
signOut: '/auth/signout',
},
theme: {
colorScheme: "light",
},
...
}
Site Connection File
The list, update, add, and delete API calls are from a secure page under ‘.\pages\auth\comments\index.js’.

For the demo application in GitHub, there is a file with all of the connection strings under ‘.\lib\SiteConn.js’. The connection string for the Comments and Northwind are included.
Code for the SiteConn.js file.
export default {
get_config: function(){
return {
BASE_URL: "http://localhost:3000",
NorthwindDbFileName: "northwind.db",
CommentDbFileName: "comments.db",
}
},
}
API CRUD Operations
The list.js, update.js, add.js, and delete.js API routes are called by the secure page ‘.\pages\auth\comments\index.js’.
A nicer way to do this, would be to put all the CRUD operations in one file and use GET, POST, PUT, and DELETE,

List Comments
Get a list of all the comments from the comments table. The APS is located ‘.\pages\comments\list’ and called by ‘.\pages\comments’.
const sqlite = require('sqlite');
const sqlite3= require('sqlite3');
import {open} from 'sqlite';
import LibConst from "@/libs/SiteConn";
export default async function (req, res){
try{
var dbFile = LibConst.get_config().CommentDbFileName
// console.log(d)
const db = await open(
{filename: dbFile , driver: sqlite3.Database}
);
const sqlQuery = 'SELECT c.CommentId, c.UserId, c.Content, ' +
'c.Replies, c.IsActive, c.DateCreated, c.DateModified,' +
'u.UserId, u.Name, u.Picture ' +
'FROM Comments c, Users u WHERE c.UserId = u.UserId ORDER BY c.CommentId DESC';
const items = await db.all(sqlQuery); var ret ={
items: items
}
res.status(200).json(ret);
} catch (err) {
console.error(err);
res.status(500).send();
}
};
Update Comment
Updates a single record from the comments table. The APS is located ‘.\pages\comments\delete’ and called by ‘.\pages\comments\index.js’.
const sqlite = require('sqlite');
const sqlite3= require('sqlite3');
import {open} from 'sqlite';
import LibConst from "@/libs/SiteConn";
//
export default async function (req, res){
try{
console.log('API', req.body);
var data = req.body
var dbFile = LibConst.get_config().CommentDbFileName
const db = await open(
{filename: dbFile , driver: sqlite3.Database}
);
const result = await db.run(
'UPDATE Comments SET Content = ? WHERE CommentId = ?',
data.Content,
data.CommentId
)
res.json(result);
} catch (err) {
console.log(err);
res.status(500).send();
}
};
Add Comment
Adds a single record from the comments table. The APS is located ‘.\pages\comments\delete’ and called by ‘.\pages\comments\index.js’.
const sqlite = require('sqlite');
const sqlite3= require('sqlite3');
import {open} from 'sqlite';
import moment from 'moment';
import LibConst from "@/libs/SiteConn";
//
export default async function (req, res){
try{
var data = req.body
var today = new Date();
const createData = moment(today).format('YYYY-MM-DD HH:MM:SS');
var dbFile = LibConst.get_config().CommentDbFileName
const db = await open(
{filename: dbFile , driver: sqlite3.Database}
);
const result = await db.run(
'INSERT INTO Comments (UserId, Content, Replies, IsActive, DateCreated) VALUES ( ?, ?, ?, ?, ?)',
data.UserId,
data.Content,
0,
1,
createData,
)
const sqlSelectQuery = 'SELECT c.CommentId, c.UserId, c.Content, ' +
'c.Replies, c.IsActive, c.DateCreated, c.DateModified,' +
'u.UserId, u.Name, u.Picture ' +
'FROM Comments c, Users u WHERE c.UserId = u.UserId ORDER BY c.CommentId DESC';
const items = await db.all(sqlSelectQuery);
var ret ={
items: items
}
res.json(ret);
} catch (err) {
console.log(err);
res.status(500).send();
}
};
Delete Comment
Deletes a single record from the comments table. The APS is located ‘.\pages\comments\delete’ and called by ‘.\pages\comments\index.js’.
const sqlite = require('sqlite');
const sqlite3= require('sqlite3');
import {open} from 'sqlite';
import LibConst from "@/libs/SiteConn";
//* D E L E T E A S I N G L E C O M M E N T
export default async function (req, res){
try{
console.log('DELETE API', req.body);
var data = req.body
var dbFile = LibConst.get_config().CommentDbFileName
const db = await open(
{filename: dbFile , driver: sqlite3.Database}
);
const result = await db.run(
'DELETE FROM Comments WHERE CommentId = ?',
data.CommentId,
)
const sqlSelectQuery = 'SELECT c.CommentId, c.UserId, c.Content, ' +
'c.Replies, c.IsActive, c.DateCreated, c.DateModified,' +
'u.UserId, u.Name, u.Picture ' +
'FROM Comments c, Users u WHERE c.UserId = u.UserId ORDER BY c.CommentId DESC';
const items = await db.all(sqlSelectQuery);
var ret ={
items: items
}
res.json(ret);
res.json(ret);
} catch (err) {
console.log(err);
res.status(500).send();
}
};
Securing Pages with Middleware
Using NextJS ‘.\middleware.ts’ file at the root of the application.

You can secure folders and individual pages with Next-Auth using middleware.ts. The following line secures everything under the ‘.\pages\auth\’ and ‘.\pages\profile’ folders.
export const config = { matcher: ["/auth/:path*", "/profile"] }
Code for middleware.ts.
import { withAuth } from "next-auth/middleware"
// More on how NextAuth.js middleware works: https://next-auth.js.org/configuration/nextjs#middleware
export default withAuth({
callbacks: {
authorized({ req, token }) {
// `/admin` requires admin role
if (req.nextUrl.pathname === "/admin") {
return token?.userRole === "admin"
}
// `/me` only requires the user to be logged in
return !!token
},
},
})
export const config = { matcher: "/auth/:path*", "/profile"] }
Login Page


Sign In Page CSS Styling
This example uses a custom login page which requires an option enabled
.main{
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: url('/background.avif') fixed no-repeat;
background-size: cover;
}
#container{
width: 350px;
height: 500px;
background: inherit;
position: absolute;
overflow: hidden;
top: 50%;
left: 50%;
margin-left: -175px;
margin-top: -250px;
border-radius: 8px;
}
#container:before{
width: 400px;
height: 550px;
content: "";
position: absolute;
top: -25px;
left: -25px;
bottom: 0;
right: 0;
background: inherit;
box-shadow: inset 0 0 0 200px rgba(255,255,255,0.2);
filter: blur(10px);
}
.form img{
width: 120px;
height: 120px;
border-radius: 100%;
}
.form{
text-align: center;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}
.input{
background: 0;
width: 200px;
outline: 0;
border: 0;
border-bottom: 2px solid rgba(255,255,255, 0.3);
margin: 20px 0;
padding-bottom: 10px;
font-size: 18px;
font-weight: bold;
color: rgba(255,255,255, 0.8);
}
.input[type="submit"]{
border: 0;
border-radius: 8px;
padding-bottom: 0;
height: 60px;
background: #df2359;
color: white;
cursor: pointer;
transition: all 600ms ease-in-out;
}
.input[type="submit"]:hover{
background: #C0392B;
}
.span a{
color: rgba(255,255,255, 0.8);
}
Sign In Page Code
import {useState, useEffect} from 'react';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import CssBaseline from '@mui/material/CssBaseline';
import Paper from '@mui/material/Paper';
import Grid from '@mui/material/Grid';
import Box from '@mui/material/Box';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import Typography from '@mui/material/Typography';
import { useRouter } from 'next/router';
import { Stack } from '@mui/material';
import GoogleIcon from '@mui/icons-material/Google';
import GitHubIcon from '@mui/icons-material/GitHub';
import UserIcon from '@mui/icons-material/Person';
import { getProviders, signIn, getSession } from "next-auth/react";
import { getToken } from "next-auth/jwt"
import compStyle from './login.module.css'
export default function Signin({ token, csrfToken, providers }) {
const router = useRouter();
useEffect(() => {
if(token !== null) router.push('/auth/comments')
}, []);
const [values, setValues] = useState({
email: '',
password: '',
pin: '',
showPassword: false
});
const [showAlert, setShowAlert] = useState(false);
const getProviderIcon = (provider) => {
console.log('provider', provider)
if(provider === 'Google') return <GoogleIcon/>
if(provider === 'GitHub') return <GitHubIcon/>
else return <UserIcon/>
}
console.log('token', token)
return (
<div className={compStyle.main}>
<div id={compStyle.container}>
<CssBaseline />
<Paper elevation={2}>
<Box
blur={10}
sx={{
padding: 2,
marginTop: 8,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Avatar >
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5" style={{color: 'white', zIndex: 99}}>
Sign in
</Typography>
<form noValidate>
<Grid container spacing={2}>
<Grid item xs={12}>
<Stack spacing={2} sx={{mt: 8, mb: 4}}>
{Object.values(providers).map((provider) => (
<div key={provider.name}>
<Button size="large" variant="contained" fullWidth onClick={() => signIn(provider.id)} startIcon={getProviderIcon(provider.name)}>
Sign in as {provider.name}
</Button>
</div>
))}
</Stack>
</Grid>
</Grid>
</form>
</Box>
</Paper>
</div>
</div>
);
}
export async function getServerSideProps(context) {
const { req, res } = context
try {
const secret = process.env.NEXTAUTH_SECRET
const token = await getToken({ req, secret })
return { props: { token: token, providers: await getProviders() } };
} catch (e) {
return { props: { providers: await getProviders() } };
}
}
Components for the Comments Page

Comment Card

Code for CommentCard.js
import * as React from 'react';
import moment from 'moment';
import { Avatar, Grid, Paper, Button, TextField } from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';
import SaveIcon from '@mui/icons-material/Save';
import ClearIcon from '@mui/icons-material/Clear';
import DialogDeleteComment from './DialogDeleteComment'
import Stack from '@mui/material/Stack';
import axiosConn from '@/src/axiosConn'
//https://randomuser.me/api/ results.picture.thumbnail results.name.first results.name.last
export default function CommentCard(props) {
const [isEditComment, setIsEditComment] = React.useState(false);
const [comment, setComment] = React.useState('');
const [isRepliesExpanded, setIsRepliesExpanded] = React.useState(false);
const [replyToComment, setReplyToComment] = React.useState('');
const [replySkeletonCount, setReplySkeletonCount] = React.useState([]);
const [openDeleteDialog, setOpenDeleteDialog] = React.useState(false);
const [openAddReply, setOpenAddReply] = React.useState(false);
const [repliesData, setRepliesData] = React.useState([]);
const [isRepliesDataLoaded, setIsRepliesDataLoaded] = React.useState(false);
const [repliesCount, setRepliesCount] = React.useState(0);
//* I N I T
React.useEffect(() => {
setComment(props.data.Content)
var tmpCntArr = []
for (let i = 0; i < props.data.Replies; i++) {
tmpCntArr.push(i)
}
setReplySkeletonCount(tmpCntArr)
setRepliesCount(props.data.Replies);
}, []);
//* H A N D L E E N A B L E E D I T C O M M E N T
const handleEnableEditComment = (boolVal) => {
setIsEditComment(boolVal)
}
//* H A N D L E C A N C E L E D I T
const handleCancelEdit = () => {
setComment(props.data.Content)
setIsEditComment(false)
}
//* H A N D L E O P E N D I A L O G
const handleDialogOpen = () => {
setOpenDeleteDialog(true);
};
//* H A N D L E C L O S E D I A L O G
const handleDialogClose = () => {
setOpenDeleteDialog(false);
};
//* H A N D L E D I A L O G D E L E T E C O M M E N T
const handleDialogDeleteComment = (ID) => {
setOpenDeleteDialog(false);
props.handleDeleteComment(props.data.CommentId)
};
//* H A N D L E S A V E E D I T
const handleSaveEdit = (commentStr) => {
axiosConn.post('/api/comments/update', { CommentId: props.data.CommentId, Content: comment })
.then((data) => {
console.log(data); // JSON data parsed by `data.json()` call
setRepliesData(data.data.data)
setRepliesCount(repliesCount + 1)
setIsEditComment(false);
});
}
var momentDate=new moment(props.data.DateCreated);
return (
<div style={{ padding: 14 }} className="App">
<Paper style={{ padding: "10px 10px", marginTop: 10 }}>
<Grid container wrap="nowrap" spacing={2}>
<Grid item justifyContent="left">
<Avatar alt={props.data.Name} src={props.data.Picture} />
</Grid>
<Grid justifyContent="left" item xs zeroMinWidth>
<Grid container wrap="nowrap" spacing={2}>
<Grid justifyContent="right" item xs zeroMinWidth>
<h4 style={{ margin: 0, textAlign: "left" }}>{props.data.Name}</h4>
</Grid>
<Grid justifyContent="right" item>
{props.data.UserId == props.currentUserId &&
<>
{isEditComment ?
<>
<Button variant="text" color="warning" startIcon={<ClearIcon/>} onClick={() => handleCancelEdit()}>Cancel</Button>
<Button variant="text" color="secondary" startIcon={<SaveIcon/>} onClick={() => handleSaveEdit()}>Save</Button>
</>
:
<>
<Stack direction="row" spacing={2}>
<DialogDeleteComment
handleDialogOpen={handleDialogOpen}
handleDialogClose={handleDialogClose}
handleDialogDeleteComment={handleDialogDeleteComment}
openDeleteDialog={openDeleteDialog}/>
<Button variant="text" color="primary" startIcon={<EditIcon/>} onClick={() => handleEnableEditComment(true)}>Edit</Button>
</Stack>
</>
}
</>
}
</Grid>
</Grid>
{isEditComment ?
<>
<TextField
fullWidth
id="filled-multiline-static"
label=""
multiline
rows={4}
defaultValue=""
variant="outlined"
value={comment}
onChange={(e) => setComment(e.target.value)}
/>
</>
:
<>
<p style={{ textAlign: "left" }}>
{comment}
</p>
</>
}
<Grid container wrap="nowrap" spacing={6}>
<Grid item align="left" xs={2}>
<p style={{ textAlign: "left", color: "gray" }}>
Posted {moment(momentDate).fromNow(true)} ago
</p>
</Grid>
</Grid>
</Grid>
</Grid>
</Paper>
</div>
);
}
Comment Skeleton Component
Comment Skeleton Component
import * as React from 'react';
import { Grid, Paper} from '@mui/material';
import Skeleton from '@mui/material/Skeleton';
export default function CommentSkeleton(props) {
return (
<div style={{ padding: 14 }} className="App">
<Paper style={{ padding: "10px 10px", marginTop: 10 }}>
<Grid container wrap="nowrap" spacing={2}>
<Grid item justifyContent="left">
<Skeleton animation="wave" variant="circular" width={40} height={40} />
</Grid>
<Grid justifyContent="left" item xs zeroMinWidth>
<Grid container wrap="nowrap" spacing={2}>
<Grid justifyContent="right" item xs zeroMinWidth>
<Skeleton animation="wave" height={30} width="10%" style={{ marginBottom: 6 }}/>
</Grid>
<Grid justifyContent="right" item>
<Skeleton animation="wave" height={30} width="100%" style={{ marginRight: 66 }} />
</Grid>
</Grid>
<Skeleton animation="wave" height={25} width="80%" />
<p style={{ textAlign: "left", color: "gray" }}>
<Skeleton animation="wave" height={25} width="30%" />
</p>
</Grid>
</Grid>
</Paper>
</div>
);
}
New Comment Card Component

Code for NewComment.js.
import React from "react";
import ReactDOM from "react-dom";
import { Divider, Avatar, Grid, Paper, TextField, Button } from '@mui/material';
// "https://images.pexels.com/photos/1681010/pexels-photo-1681010.jpeg?auto=compress&cs=tinysrgb&dpr=3&h=750&w=1260";
export default function CommentCard(props) {
console.log('props.userSession', props.userSession)
const imgLink = props.userSession.picture;
return (
<div style={{ padding: 14 }} className="App">
<Paper style={{ padding: "10px 10px", marginTop: 10 }} elevation={3}>
<Grid container wrap="nowrap" spacing={2} style={{padding: 8}}>
<Grid item>
<Avatar alt={''} src={imgLink} />
</Grid>
<Grid justifyContent="left" item xs zeroMinWidth>
<TextField
fullWidth
id="filled-multiline-static"
placeholder="Add a comment"
label=""
multiline
rows={4}
defaultValue=""
variant="outlined"
value={props.newComment}
onChange={(e) => props.setNewComment(e.target.value)}
/>
</Grid>
<Grid item>
<Button variant="contained" color="primary" onClick={props.handleNewComment}>Send</Button>
</Grid>
</Grid>
</Paper>
</div>
);
}
Delete Comment Dialog Component
Code for DialogDeleteCommenty.js.
import * as React from 'react';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import useMediaQuery from '@mui/material/useMediaQuery';
import { useTheme } from '@mui/material/styles';
import DeleteIcon from '@mui/icons-material/Delete';
export default function DialogDeleteComment(props) {
const [open, setOpen] = React.useState(false);
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down('md'));
return (
<div>
<Button variant="text" color="warning" onClick={props.handleDialogOpen} startIcon={<DeleteIcon/>} >
Delete
</Button>
<Dialog
fullScreen={fullScreen}
open={props.openDeleteDialog}
onClose={props.handleDialogClose}
aria-labelledby="responsive-dialog-title"
>
<DialogTitle id="responsive-dialog-title">
{"Are you sure?"}
</DialogTitle>
<DialogContent>
<DialogContentText>
Do you want to delete this comment?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button variant="text" autoFocus onClick={props.handleDialogClose} color="warning">
No
</Button>
<Button onClick={(e) =>props.handleDialogDeleteComment(e)} >
Yes
</Button>
</DialogActions>
</Dialog>
</div>
);
}
Comments Page
The comments page is under


import React, {useEffect, useState} from "react";
import Head from 'next/head';
import { styled } from '@mui/material/styles';
import { Container, Typography } from '@mui/material';
import CommentCard from '@/components/comments/CommentCard';
import NewComment from '@/components/comments/NewComment';
import CommentSkeleton from '@/components/comments/CommentSkeleton';
import { Grid, Paper } from '@mui/material';
import axiosConn from '@/src/axiosConn'
import { getToken } from "next-auth/jwt";
export default function Comments({ token, ssrData }) {
const [newComment, setNewComment] = useState('');
const [commentsData, setCommentsData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
//* I N I T
useEffect(() => {
// console.log(ssrData.items)
setCommentsData(ssrData.items)
setTimeout(() => {
setIsLoading(false);
}, 600);
}, []);
//* H A N D L E N E W C O M M E N T
const handleNewComment = async () => {
var tmpArr = []
var id=0
axiosConn.post('/api/comments/add', { UserId: Number(token.sub), Content: newComment })
.then((data) => {
// console.log(data); // JSON data parsed by `data.json()` call
// setOpen(false);
commentsData.forEach(elem => {
tmpArr.push(elem)
id++
})
tmpArr.push({Id: 2, UserId: 22, Name: 'John Smith', Content: newComment})
setCommentsData(tmpArr)
setNewComment('')
});
}
//* H A N D L E D E L E T E C O M M E N T
const handleDeleteComment = (ID) => {
console.log('handleDeleteComment', ID)
axiosConn.post('/api/comments/delete', { CommentId: ID })
.then((data) => {
setCommentsData(data.data.items)
});
}
//* H A N D L E S A V E E D I T
const handleSaveReplyEdit = (ID, commentStr) => {
setCommentsData(tmpArr)
}
//* R E N D E R
return (
<>
<Head>
<meta name="viewport" content="initial-scale=1, width=device-width" />
</Head>
<Container maxWidth="lg">
<Typography variant="h6">
Comments Section
</Typography>
<Grid container spacing={1}>
{isLoading ?
<>
{[1,2,3,4].map(item => (
<Grid item xs={12} key={item}>
<CommentSkeleton key={item}/>
</Grid>
))}
</>
:
<>
{commentsData.map((item, index) => (
<Grid item xs={12} key={index}>
<CommentCard currentUserId={token.sub}
data={item}
key={index}
handleDeleteComment={handleDeleteComment}
handleSaveReplyEdit={handleSaveReplyEdit}
//handleDeleteReply={handleDeleteReply}
/>
</Grid>
))}
<Grid item xs={12}>
<NewComment handleNewComment={handleNewComment}
userName={'John Smith'}
setNewComment={setNewComment}
newComment={newComment}/>
</Grid>
</>
}
</Grid>
</Container>
</>
);
}
export async function getServerSideProps(context) {
const { req, res } = context;
const secret = process.env.NEXTAUTH_SECRET
const token = await getToken({ req, secret })
const result = await fetch('http://localhost:3000/api/comments/list')
const ssrData = await result.json()
return {
props: {
ssrData: ssrData,
token: token
},
}
}
Server Side Data Fetching & User Info
The code below fetches the user token and comments data.
User and Token Info
Below is the authenticated user token.
{
"name": "Jennifer Smith",
"email": "jennifers@example.com",
"picture": "https://randomuser.me/api/portraits/women/72.jpg",
"sub": "1",
"userRole": "admin",
"iat": 1674165138,
"exp": 1676757138,
"jti": "483afcb9-78bf-4205-ac59-1f95d8f23f69"
}
Code
The code for fetching the user token and comments data.
export async function getServerSideProps(context) {
const { req, res } = context;
const secret = process.env.NEXTAUTH_SECRET
const token = await getToken({ req, secret })
const result = await fetch('http://localhost:3000/api/comments/list')
const ssrData = await result.json()
return {
props: {
ssrData: ssrData,
token: token
},
}
...
CommentRepliesParentLayout.js
//* H A N D L E N E W C O M M E N T
const handleNewComment = async () => {
var tmpArr = []
var id=0
axiosConn.post('/api/comments/add', { UserId: Number(token.sub), Content: newComment })
.then((data) => {
commentsData.forEach(elem => {
tmpArr.push(elem)
id++
})
tmpArr.push({Id: 2, UserId: 22, Name: 'John Smith', Content: newComment})
setCommentsData(tmpArr)
setNewComment('')
});
}
//* H A N D L E D E L E T E C O M M E N T
const handleDeleteComment = (ID) => {
console.log('handleDeleteComment', ID)
axiosConn.post('/api/comments/delete', { CommentId: ID })
.then((data) => {
setCommentsData(data.data.items)
});
}
//* H A N D L E S A V E E D I T
const handleSaveReplyEdit = (ID, commentStr) => {
setCommentsData(tmpArr)
}
//* H A N D L E S A V E E D I T
const handleSaveReplyEdit = (ID, commentStr) => {
setCommentsData(tmpArr)
}
CommentRepliesParentLayout.js
import React, {useEffect, useState} from "react";
import Head from 'next/head';
import { styled } from '@mui/material/styles';
import { Container, Typography } from '@mui/material';
import CommentCard from '@/components/comments/CommentCard';
import NewComment from '@/components/comments/NewComment';
import CommentSkeleton from '@/components/comments/CommentSkeleton';
import { Grid, Paper } from '@mui/material';
import axiosConn from '@/src/axiosConn'
import { getToken } from "next-auth/jwt";
export default function Comments({ token, ssrData }) {
const [newComment, setNewComment] = useState('');
const [commentsData, setCommentsData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
//* I N I T
useEffect(() => {
// console.log(ssrData.items)
setCommentsData(ssrData.items)
setTimeout(() => {
setIsLoading(false);
}, 600);
}, []);
//* H A N D L E N E W C O M M E N T
const handleNewComment = async () => {
var tmpArr = []
var id=0
axiosConn.post('/api/comments/add', { UserId: Number(token.sub), Content: newComment })
.then((data) => {
// console.log(data); // JSON data parsed by `data.json()` call
// setOpen(false);
commentsData.forEach(elem => {
tmpArr.push(elem)
id++
})
tmpArr.push({Id: 2, UserId: 22, Name: 'John Smith', Content: newComment})
setCommentsData(tmpArr)
setNewComment('')
});
}
//* H A N D L E D E L E T E C O M M E N T
const handleDeleteComment = (ID) => {
console.log('handleDeleteComment', ID)
axiosConn.post('/api/comments/delete', { CommentId: ID })
.then((data) => {
setCommentsData(data.data.items)
});
}
//* H A N D L E S A V E E D I T
const handleSaveReplyEdit = (ID, commentStr) => {
setCommentsData(tmpArr)
}
//* R E N D E R
return (
<>
<Head>
<meta name="viewport" content="initial-scale=1, width=device-width" />
</Head>
<Container maxWidth="lg">
<Typography variant="h6">
Comments Section
</Typography>
<Grid container spacing={1}>
{isLoading ?
<>
{[1,2,3,4].map(item => (
<Grid item xs={12} key={item}>
<CommentSkeleton key={item}/>
</Grid>
))}
</>
:
<>
{commentsData.map((item, index) => (
<Grid item xs={12} key={index}>
<CommentCard currentUserId={token.sub}
data={item}
key={index}
handleDeleteComment={handleDeleteComment}
handleSaveReplyEdit={handleSaveReplyEdit}
//handleDeleteReply={handleDeleteReply}
/>
</Grid>
))}
<Grid item xs={12}>
<NewComment handleNewComment={handleNewComment}
userName={'John Smith'}
setNewComment={setNewComment}
newComment={newComment}/>
</Grid>
</>
}
</Grid>
</Container>
</>
);
}
export async function getServerSideProps(context) {
const { req, res } = context;
const secret = process.env.NEXTAUTH_SECRET
const token = await getToken({ req, secret })
const result = await fetch('http://localhost:3000/api/comments/list')
const ssrData = await result.json()
return {
props: {
ssrData: ssrData,
token: token
},
}
}
CommentReplies.js
CommentReplyBox.js
CommentParentLayout.js
Conclusion
This NextJS CRUD application is a good candidate for a state management library like React-Query. The example application from GitHub has React-Query installed and configured, just in case you want to try it yourself. Otherwise, I will write another article on how to set up and use React-Query for this application.
Another challenge is to add replies to any comment. Adding Replies is a bit more complex since you must keep track of how many there are and collapse/expand the child records for each comment.

You must be logged in to post a comment.