Let's Build a Full-Stack App Using the MERN Stack! Part 3: React NextJS UI
Now that we have the database and backend set up, let's create the UI. We're using the MERN stack, where "R" stands for React. We'll be using Next.js, a React framework that extends React's capabilities. Setting Up Next.js To create a Next.js application, run the following command: npx create-next-app@latest You'll be prompted with several configuration choices: Ok to proceed? → y What is your project named? → ui Would you like to use TypeScript? → Yes Would you like to use ESLint? → Yes Would you like to use Tailwind CSS? → Yes Would you like your code inside a src/ directory? → No Would you like to use App Router? → Yes Would you like to use Turbopack for next dev? → No Would you like to customize the import alias (@/* by default)? → No Start the Development Server Navigate into the ui folder and run the app: cd ui npm run dev Install Axios Next, install Axios to handle API requests: npm install axios Clean Up page.tsx Delete everything inside page.tsx, so it looks like this: export default function Home() { return ( Things ); } Now, let's set next.js to 'use client', add imports, and create our interfaces. In Next.js 13, components are server components by default. Server components are rendered on the server and sent as static HTML to the client. They are great for performance and SEO but cannot use client-side features such as react hooks. 'use client' import { useEffect, useState } from 'react'; import axios from 'axios'; interface Thing { _id: string; title: string; description: string; } Now, add the following methods to the default Home function. const [things, setThings] = useState([]); const [selectedThing, setSelectedThing] = useState(null); // Base API endpoint const API_BASE_URL = 'http://localhost:5000/things'; useEffect(() => { getTasks(); }, []); // Handles radio button click and populates the form with the selected thing's data const handleRadioClick = (thing: Thing) => { setSelectedThing(thing); populateForm(thing); }; // Populates the form fields with the data of the selected thing const populateForm = (thing: Thing) => { const form = document.querySelector('form'); if (form) { (form.querySelector('input[name="title"]') as HTMLInputElement).value = thing.title; (form.querySelector('input[name="description"]') as HTMLInputElement).value = thing.description; } }; // Clears the selected thing and resets the form and radio button selection const clearSelectedThing = () => { setSelectedThing(null); clearForm(); clearRadioSelection(); }; // Clears the form fields const clearForm = () => { const form = document.querySelector('form'); if (form) { (form.querySelector('input[name="title"]') as HTMLInputElement).value = ''; (form.querySelector('input[name="description"]') as HTMLInputElement).value = ''; } }; // Clears the radio button selection const clearRadioSelection = () => { const radios = document.querySelectorAll('input[type="radio"]'); radios.forEach(radio => (radio as HTMLInputElement).checked = false); }; // Fetches the list of tasks from the server const getTasks = async () => { try { const res = await axios.get(API_BASE_URL); setThings(res.data); } catch (error) { console.error('Error fetching tasks:', error); } }; // Adds or edits a task based on the form data const addEditTask = async (event: React.FormEvent) => { event.preventDefault(); const form = event.target as HTMLFormElement; const formData = new FormData(form); const title = formData.get('title') as string; const description = formData.get('description') as string; try { if (selectedThing) { const res = await axios.put(`${API_BASE_URL}/${selectedThing._id}`, { title, description }); setThings(things.map(thing => (thing._id === selectedThing._id ? res.data : thing))); } else { const res = await axios.post(API_BASE_URL, { title, description }); setThings([...things, res.data]); } clearSelectedThing(); } catch (error) { console.error('Error adding/editing task:', error); } }; // Deletes the selected task const deleteSelectedThing = async () => { if (selectedThing) { try { await axios.delete(`${API_BASE_URL}/${selectedThing._id}`); setThings(things.filter(thing => thing._id !== selectedThing._id)); clearSelectedThing(); } catch (error) { console.error('Error deleting task:', error); } } }; Finally, replace all the html being returned with the following code. Things All Tasks {things.map((thing) => ( handleRadioClick(thing)} /> {thing.title} - {thing.description} ))} {selectedThing === null ? 'Add' : 'Edit'} Task Name: Description: {selectedThing === null ? 'Add Task' : 'Edit Task'} Clear Selected Thing Delete Selected Thing Bam!

Now that we have the database and backend set up, let's create the UI. We're using the MERN stack, where "R" stands for React. We'll be using Next.js, a React framework that extends React's capabilities.
Setting Up Next.js
To create a Next.js application, run the following command:
npx create-next-app@latest
You'll be prompted with several configuration choices:
- Ok to proceed? → y
- What is your project named? → ui
- Would you like to use TypeScript? → Yes
- Would you like to use ESLint? → Yes
- Would you like to use Tailwind CSS? → Yes
- Would you like your code inside a src/ directory? → No
- Would you like to use App Router? → Yes
- Would you like to use Turbopack for next dev? → No
- Would you like to customize the import alias (@/* by default)? → No
Start the Development Server
Navigate into the ui folder and run the app:
cd ui
npm run dev
Install Axios
Next, install Axios to handle API requests:
npm install axios
Clean Up page.tsx
Delete everything inside page.tsx, so it looks like this:
export default function Home() {
return (
Things
);
}
Now, let's set next.js to 'use client', add imports, and create our interfaces. In Next.js 13, components are server components by default. Server components are rendered on the server and sent as static HTML to the client. They are great for performance and SEO but cannot use client-side features such as react hooks.
'use client'
import { useEffect, useState } from 'react';
import axios from 'axios';
interface Thing {
_id: string;
title: string;
description: string;
}
Now, add the following methods to the default Home function.
const [things, setThings] = useState([]);
const [selectedThing, setSelectedThing] = useState(null);
// Base API endpoint
const API_BASE_URL = 'http://localhost:5000/things';
useEffect(() => {
getTasks();
}, []);
// Handles radio button click and populates the form with the selected thing's data
const handleRadioClick = (thing: Thing) => {
setSelectedThing(thing);
populateForm(thing);
};
// Populates the form fields with the data of the selected thing
const populateForm = (thing: Thing) => {
const form = document.querySelector('form');
if (form) {
(form.querySelector('input[name="title"]') as HTMLInputElement).value = thing.title;
(form.querySelector('input[name="description"]') as HTMLInputElement).value = thing.description;
}
};
// Clears the selected thing and resets the form and radio button selection
const clearSelectedThing = () => {
setSelectedThing(null);
clearForm();
clearRadioSelection();
};
// Clears the form fields
const clearForm = () => {
const form = document.querySelector('form');
if (form) {
(form.querySelector('input[name="title"]') as HTMLInputElement).value = '';
(form.querySelector('input[name="description"]') as HTMLInputElement).value = '';
}
};
// Clears the radio button selection
const clearRadioSelection = () => {
const radios = document.querySelectorAll('input[type="radio"]');
radios.forEach(radio => (radio as HTMLInputElement).checked = false);
};
// Fetches the list of tasks from the server
const getTasks = async () => {
try {
const res = await axios.get(API_BASE_URL);
setThings(res.data);
} catch (error) {
console.error('Error fetching tasks:', error);
}
};
// Adds or edits a task based on the form data
const addEditTask = async (event: React.FormEvent) => {
event.preventDefault();
const form = event.target as HTMLFormElement;
const formData = new FormData(form);
const title = formData.get('title') as string;
const description = formData.get('description') as string;
try {
if (selectedThing) {
const res = await axios.put(`${API_BASE_URL}/${selectedThing._id}`, { title, description });
setThings(things.map(thing => (thing._id === selectedThing._id ? res.data : thing)));
} else {
const res = await axios.post(API_BASE_URL, { title, description });
setThings([...things, res.data]);
}
clearSelectedThing();
} catch (error) {
console.error('Error adding/editing task:', error);
}
};
// Deletes the selected task
const deleteSelectedThing = async () => {
if (selectedThing) {
try {
await axios.delete(`${API_BASE_URL}/${selectedThing._id}`);
setThings(things.filter(thing => thing._id !== selectedThing._id));
clearSelectedThing();
} catch (error) {
console.error('Error deleting task:', error);
}
}
};
Finally, replace all the html being returned with the following code.
Things
All Tasks
{things.map((thing) => (
-
handleRadioClick(thing)}
/>
))}
{selectedThing === null ? 'Add' : 'Edit'} Task
Bam! There you go! You now have full CRUD applications.