Building the React Frontend
Last updated September 18, 2023
JavaScript

Guide Parts
Building the React Frontend
In this section, we'll build a responsive React frontend for our task management application. We'll create components, set up routing, implement authentication, and connect to our backend API.
Project Structure
First, let's organize our React project structure:
client/
├── public/
├── src/
│ ├── assets/
│ ├── components/
│ │ ├── auth/
│ │ ├── layout/
│ │ ├── tasks/
│ │ └── ui/
│ ├── context/
│ ├── pages/
│ ├── services/
│ ├── utils/
│ ├── App.js
│ ├── index.js
│ └── routes.js
└── package.json
Let's create these directories:
cd client/src
mkdir -p assets components/{auth,layout,tasks,ui} context pages services utils
Setting Up Authentication Context
Let's create a context for managing authentication state:
touch src/context/AuthContext.js
// src/context/AuthContext.js
import { createContext, useReducer, useContext, useEffect } from "react";
import axios from "axios";
// Create context
const AuthContext = createContext();
// Initial state
const initialState = {
user: null,
token: localStorage.getItem("token"),
isAuthenticated: false,
loading: true,
error: null,
};
// Reducer function
const authReducer = (state, action) => {
switch (action.type) {
case "LOGIN_SUCCESS":
case "REGISTER_SUCCESS":
localStorage.setItem("token", action.payload.token);
return {
...state,
user: action.payload.user,
token: action.payload.token,
isAuthenticated: true,
loading: false,
error: null,
};
case "AUTH_ERROR":
case "LOGIN_FAIL":
case "REGISTER_FAIL":
case "LOGOUT":
localStorage.removeItem("token");
return {
...state,
user: null,
token: null,
isAuthenticated: false,
loading: false,
error: action.payload,
};
case "USER_LOADED":
return {
...state,
user: action.payload,
isAuthenticated: true,
loading: false,
};
case "CLEAR_ERRORS":
return {
...state,
error: null,
};
default:
return state;
}
};
// Provider component
export const AuthProvider = ({ children }) => {
const [state, dispatch] = useReducer(authReducer, initialState);
// Set auth token as default header
if (state.token) {
axios.defaults.headers.common["Authorization"] = `Bearer ${state.token}`;
} else {
delete axios.defaults.headers.common["Authorization"];
}
// Load user if token exists
useEffect(() => {
const loadUser = async () => {
if (!state.token) {
dispatch({ type: "AUTH_ERROR" });
return;
}
try {
const res = await axios.get(`${process.env.REACT_APP_API_URL}/auth/me`);
dispatch({ type: "USER_LOADED", payload: res.data.user });
} catch (error) {
dispatch({ type: "AUTH_ERROR" });
}
};
loadUser();
}, [state.token]);
// Register user
const register = async (formData) => {
try {
const res = await axios.post(
`${process.env.REACT_APP_API_URL}/auth/register`,
formData
);
dispatch({ type: "REGISTER_SUCCESS", payload: res.data });
} catch (error) {
dispatch({
type: "REGISTER_FAIL",
payload: error.response?.data?.message || "Registration failed",
});
}
};
// Login user
const login = async (formData) => {
try {
const res = await axios.post(
`${process.env.REACT_APP_API_URL}/auth/login`,
formData
);
dispatch({ type: "LOGIN_SUCCESS", payload: res.data });
} catch (error) {
dispatch({
type: "LOGIN_FAIL",
payload: error.response?.data?.message || "Invalid credentials",
});
}
};
// Logout user
const logout = () => {
dispatch({ type: "LOGOUT" });
};
// Clear errors
const clearErrors = () => {
dispatch({ type: "CLEAR_ERRORS" });
};
return (
<AuthContext.Provider
value={{
user: state.user,
token: state.token,
isAuthenticated: state.isAuthenticated,
loading: state.loading,
error: state.error,
register,
login,
logout,
clearErrors,
}}
>
{children}
</AuthContext.Provider>
);
};
// Create custom hook for using auth context
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};
Create Tasks Context
Let's also create a context for managing tasks:
touch src/context/TaskContext.js
// src/context/TaskContext.js
import { createContext, useReducer, useContext } from "react";
import axios from "axios";
// Create context
const TaskContext = createContext();
// Initial state
const initialState = {
tasks: [],
currentTask: null,
loading: true,
error: null,
};
// Reducer function
const taskReducer = (state, action) => {
switch (action.type) {
case "GET_TASKS":
return {
...state,
tasks: action.payload,
loading: false,
};
case "GET_TASK":
return {
...state,
currentTask: action.payload,
loading: false,
};
case "ADD_TASK":
return {
...state,
tasks: [action.payload, ...state.tasks],
};
case "UPDATE_TASK":
return {
...state,
tasks: state.tasks.map((task) =>
task._id === action.payload._id ? action.payload : task
),
};
case "DELETE_TASK":
return {
...state,
tasks: state.tasks.filter((task) => task._id !== action.payload),
};
case "TASK_ERROR":
return {
...state,
error: action.payload,
loading: false,
};
case "CLEAR_CURRENT":
return {
...state,
currentTask: null,
};
default:
return state;
}
};
// Provider component
export const TaskProvider = ({ children }) => {
const [state, dispatch] = useReducer(taskReducer, initialState);
// Get all tasks
const getTasks = async () => {
try {
const res = await axios.get(`${process.env.REACT_APP_API_URL}/tasks`);
dispatch({ type: "GET_TASKS", payload: res.data.data });
} catch (error) {
dispatch({
type: "TASK_ERROR",
payload: error.response?.data?.message || "Error fetching tasks",
});
}
};
// Get a single task
const getTask = async (id) => {
try {
const res = await axios.get(
`${process.env.REACT_APP_API_URL}/tasks/${id}`
);
dispatch({ type: "GET_TASK", payload: res.data.data });
} catch (error) {
dispatch({
type: "TASK_ERROR",
payload: error.response?.data?.message || "Error fetching task",
});
}
};
// Add a task
const addTask = async (task) => {
try {
const res = await axios.post(
`${process.env.REACT_APP_API_URL}/tasks`,
task
);
dispatch({ type: "ADD_TASK", payload: res.data.data });
} catch (error) {
dispatch({
type: "TASK_ERROR",
payload: error.response?.data?.message || "Error adding task",
});
}
};
// Update a task
const updateTask = async (id, task) => {
try {
const res = await axios.put(
`${process.env.REACT_APP_API_URL}/tasks/${id}`,
task
);
dispatch({ type: "UPDATE_TASK", payload: res.data.data });
} catch (error) {
dispatch({
type: "TASK_ERROR",
payload: error.response?.data?.message || "Error updating task",
});
}
};
// Delete a task
const deleteTask = async (id) => {
try {
await axios.delete(`${process.env.REACT_APP_API_URL}/tasks/${id}`);
dispatch({ type: "DELETE_TASK", payload: id });
} catch (error) {
dispatch({
type: "TASK_ERROR",
payload: error.response?.data?.message || "Error deleting task",
});
}
};
// Clear current task
const clearCurrent = () => {
dispatch({ type: "CLEAR_CURRENT" });
};
return (
<TaskContext.Provider
value={{
tasks: state.tasks,
currentTask: state.currentTask,
loading: state.loading,
error: state.error,
getTasks,
getTask,
addTask,
updateTask,
deleteTask,
clearCurrent,
}}
>
{children}
</TaskContext.Provider>
);
};
// Create custom hook for using task context
export const useTask = () => {
const context = useContext(TaskContext);
if (!context) {
throw new Error("useTask must be used within a TaskProvider");
}
return context;
};
Create Authentication Components
Now let's create the authentication components:
touch src/components/auth/Login.js src/components/auth/Register.js
// src/components/auth/Login.js
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../../context/AuthContext";
import { TextField, Button, Typography, Box, Alert } from "@mui/material";
const Login = () => {
const [formData, setFormData] = useState({
email: "",
password: "",
});
const { email, password } = formData;
const { login, isAuthenticated, error, clearErrors } = useAuth();
const navigate = useNavigate();
useEffect(() => {
if (isAuthenticated) {
navigate("/dashboard");
}
// eslint-disable-next-line
}, [isAuthenticated]);
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = (e) => {
e.preventDefault();
clearErrors();
login({ email, password });
};
return (
<Box sx={{ maxWidth: 400, margin: "0 auto", mt: 4 }}>
<Typography variant="h4" component="h1" gutterBottom>
Login
</Typography>
{error && <Alert severity="error">{error}</Alert>}
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
<TextField
fullWidth
label="Email"
name="email"
type="email"
value={email}
onChange={handleChange}
margin="normal"
required
/>
<TextField
fullWidth
label="Password"
name="password"
type="password"
value={password}
onChange={handleChange}
margin="normal"
required
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
sx={{ mt: 3, mb: 2 }}
>
Login
</Button>
<Typography variant="body2" align="center">
Don't have an account?{" "}
<Button color="primary" onClick={() => navigate("/register")}>
Register
</Button>
</Typography>
</Box>
</Box>
);
};
export default Login;
// src/components/auth/Register.js
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../../context/AuthContext";
import { TextField, Button, Typography, Box, Alert } from "@mui/material";
const Register = () => {
const [formData, setFormData] = useState({
name: "",
email: "",
password: "",
password2: "",
});
const { name, email, password, password2 } = formData;
const { register, isAuthenticated, error, clearErrors } = useAuth();
const navigate = useNavigate();
const [localError, setLocalError] = useState(null);
useEffect(() => {
if (isAuthenticated) {
navigate("/dashboard");
}
// eslint-disable-next-line
}, [isAuthenticated]);
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = (e) => {
e.preventDefault();
setLocalError(null);
clearErrors();
if (password !== password2) {
setLocalError("Passwords do not match");
return;
}
register({ name, email, password });
};
return (
<Box sx={{ maxWidth: 400, margin: "0 auto", mt: 4 }}>
<Typography variant="h4" component="h1" gutterBottom>
Register
</Typography>
{error && <Alert severity="error">{error}</Alert>}
{localError && <Alert severity="error">{localError}</Alert>}
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
<TextField
fullWidth
label="Name"
name="name"
value={name}
onChange={handleChange}
margin="normal"
required
/>
<TextField
fullWidth
label="Email"
name="email"
type="email"
value={email}
onChange={handleChange}
margin="normal"
required
/>
<TextField
fullWidth
label="Password"
name="password"
type="password"
value={password}
onChange={handleChange}
margin="normal"
required
helperText="Password must be at least 6 characters"
/>
<TextField
fullWidth
label="Confirm Password"
name="password2"
type="password"
value={password2}
onChange={handleChange}
margin="normal"
required
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
sx={{ mt: 3, mb: 2 }}
>
Register
</Button>
<Typography variant="body2" align="center">
Already have an account?{" "}
<Button color="primary" onClick={() => navigate("/login")}>
Login
</Button>
</Typography>
</Box>
</Box>
);
};
export default Register;
Create Layout Components
touch src/components/layout/Navbar.js src/components/layout/Dashboard.js
// src/components/layout/Navbar.js
import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useAuth } from "../../context/AuthContext";
import {
AppBar,
Box,
Toolbar,
Typography,
Button,
IconButton,
Menu,
MenuItem,
Avatar,
} from "@mui/material";
import { AccountCircle } from "@mui/icons-material";
const Navbar = () => {
const { isAuthenticated, user, logout } = useAuth();
const navigate = useNavigate();
const [anchorEl, setAnchorEl] = useState(null);
const handleMenu = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleLogout = () => {
handleClose();
logout();
navigate("/");
};
const handleProfile = () => {
handleClose();
navigate("/profile");
};
return (
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static">
<Toolbar>
<Typography
variant="h6"
component={Link}
to="/"
sx={{ flexGrow: 1, textDecoration: "none", color: "inherit" }}
>
Task Manager
</Typography>
{isAuthenticated ? (
<>
<Button color="inherit" component={Link} to="/dashboard">
Dashboard
</Button>
<IconButton
size="large"
aria-label="account of current user"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={handleMenu}
color="inherit"
>
{user?.name ? (
<Avatar sx={{ width: 32, height: 32 }}>
{user.name.charAt(0).toUpperCase()}
</Avatar>
) : (
<AccountCircle />
)}
</IconButton>
<Menu
id="menu-appbar"
anchorEl={anchorEl}
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
keepMounted
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
open={Boolean(anchorEl)}
onClose={handleClose}
>
<MenuItem onClick={handleProfile}>Profile</MenuItem>
<MenuItem onClick={handleLogout}>Logout</MenuItem>
</Menu>
</>
) : (
<>
<Button color="inherit" component={Link} to="/login">
Login
</Button>
<Button color="inherit" component={Link} to="/register">
Register
</Button>
</>
)}
</Toolbar>
</AppBar>
</Box>
);
};
export default Navbar;
// src/components/layout/Dashboard.js
import { useEffect } from "react";
import { useTask } from "../../context/TaskContext";
import TaskList from "../tasks/TaskList";
import TaskForm from "../tasks/TaskForm";
import { Box, Typography, Grid, Paper } from "@mui/material";
const Dashboard = () => {
const { getTasks, loading } = useTask();
useEffect(() => {
getTasks();
// eslint-disable-next-line
}, []);
return (
<Box sx={{ mt: 4, px: 3 }}>
<Typography variant="h4" component="h1" gutterBottom>
Task Dashboard
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={5}>
<Paper sx={{ p: 3 }}>
<TaskForm />
</Paper>
</Grid>
<Grid item xs={12} md={7}>
<Paper sx={{ p: 3 }}>
<Typography variant="h5" component="h2" gutterBottom>
Your Tasks
</Typography>
{loading ? <Typography>Loading tasks...</Typography> : <TaskList />}
</Paper>
</Grid>
</Grid>
</Box>
);
};
export default Dashboard;
Create Task Components
touch src/components/tasks/TaskList.js src/components/tasks/TaskItem.js src/components/tasks/TaskForm.js
// src/components/tasks/TaskList.js
import { useTask } from "../../context/TaskContext";
import TaskItem from "./TaskItem";
import { List, Typography } from "@mui/material";
const TaskList = () => {
const { tasks } = useTask();
if (tasks.length === 0) {
return <Typography>No tasks found. Add a task to get started!</Typography>;
}
return (
<List>
{tasks.map((task) => (
<TaskItem key={task._id} task={task} />
))}
</List>
);
};
export default TaskList;
// src/components/tasks/TaskItem.js
import { useState } from "react";
import { useTask } from "../../context/TaskContext";
import {
ListItem,
ListItemText,
ListItemSecondaryAction,
IconButton,
Chip,
Box,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Typography,
} from "@mui/material";
import { Delete as DeleteIcon, Edit as EditIcon } from "@mui/icons-material";
const getPriorityColor = (priority) => {
switch (priority) {
case "high":
return "error";
case "medium":
return "warning";
case "low":
return "success";
default:
return "default";
}
};
const getStatusColor = (status) => {
switch (status) {
case "completed":
return "success";
case "in-progress":
return "warning";
case "pending":
default:
return "default";
}
};
const TaskItem = ({ task }) => {
const { deleteTask, clearCurrent, currentTask, getTask } = useTask();
const [openDialog, setOpenDialog] = useState(false);
const handleEdit = () => {
getTask(task._id);
};
const handleDelete = () => {
setOpenDialog(true);
};
const confirmDelete = () => {
deleteTask(task._id);
if (currentTask && currentTask._id === task._id) {
clearCurrent();
}
setOpenDialog(false);
};
const formatDate = (dateString) => {
const date = new Date(dateString);
return new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "short",
day: "numeric",
}).format(date);
};
return (
<>
<ListItem divider>
<ListItemText
primary={task.title}
secondary={
<Box sx={{ mt: 1 }}>
<Typography variant="body2" color="text.secondary">
{task.description}
</Typography>
<Box sx={{ mt: 1, display: "flex", gap: 1 }}>
<Chip
label={task.priority}
size="small"
color={getPriorityColor(task.priority)}
/>
<Chip
label={task.status.replace("-", " ")}
size="small"
color={getStatusColor(task.status)}
/>
{task.dueDate && (
<Chip
label={`Due: ${formatDate(task.dueDate)}`}
size="small"
variant="outlined"
/>
)}
</Box>
</Box>
}
/>
<ListItemSecondaryAction>
<IconButton edge="end" aria-label="edit" onClick={handleEdit}>
<EditIcon />
</IconButton>
<IconButton edge="end" aria-label="delete" onClick={handleDelete}>
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
<Dialog open={openDialog} onClose={() => setOpenDialog(false)}>
<DialogTitle>Confirm Delete</DialogTitle>
<DialogContent>
<Typography>
Are you sure you want to delete this task? This action cannot be
undone.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenDialog(false)}>Cancel</Button>
<Button onClick={confirmDelete} color="error">
Delete
</Button>
</DialogActions>
</Dialog>
</>
);
};
export default TaskItem;
Set Up Routes
touch src/routes.js
// src/routes.js
import {
BrowserRouter as Router,
Routes,
Route,
Navigate,
} from "react-router-dom";
import { useAuth } from "./context/AuthContext";
import Navbar from "./components/layout/Navbar";
import Dashboard from "./components/layout/Dashboard";
import Login from "./components/auth/Login";
import Register from "./components/auth/Register";
// Protected route component
const PrivateRoute = ({ children }) => {
const { isAuthenticated, loading } = useAuth();
if (loading) return <div>Loading...</div>;
return isAuthenticated ? children : <Navigate to="/login" />;
};
const AppRoutes = () => {
return (
<Router>
<Navbar />
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route
path="/dashboard"
element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
}
/>
<Route path="/" element={<Navigate to="/dashboard" />} />
</Routes>
</Router>
);
};
export default AppRoutes;
Update App.js
// src/App.js
import { ThemeProvider, createTheme } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import { AuthProvider } from "./context/AuthContext";
import { TaskProvider } from "./context/TaskContext";
import AppRoutes from "./routes";
const theme = createTheme({
palette: {
primary: {
main: "#1976d2",
},
secondary: {
main: "#dc004e",
},
},
});
function App() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<AuthProvider>
<TaskProvider>
<AppRoutes />
</TaskProvider>
</AuthProvider>
</ThemeProvider>
);
}
export default App;
Testing the Frontend
Now that we've built our frontend, we can start the application:
cd client
npm start
Your browser should open to http://localhost:3000
. You should be able to:
- Register a new account
- Login with your credentials
- Create, view, update, and delete tasks
- Logout of the application
Deployment Considerations
When deploying your full-stack application to production, consider:
- Environment Variables: Update the
.env
files for both backend and frontend - Build Process: Create a production build with
npm run build
- API Endpoints: Ensure API URLs are correct for production
- CORS Configuration: Update CORS settings on the backend
- Database Connection: Set up a production database
- Authentication: Ensure secure JWT secret and HTTPs
- Error Handling: Implement comprehensive error handling
Next Steps
To enhance your application, consider:
- Task Filtering & Sorting: Add filters by status, priority, and date
- Pagination: Implement pagination for large task lists
- Search Functionality: Add search capability
- Notifications: Add notification systems for due dates
- Team Collaboration: Allow sharing tasks between users
- Analytics: Add charts and visualizations for task completion
In this guide, we've built a complete full-stack JavaScript application with React, Node.js, Express, and MongoDB. You now have a solid foundation for developing modern web applications using the MERN stack.
Video Tutorial
For a visual walkthrough of building a React frontend, check out this helpful tutorial:
This comprehensive video covers React fundamentals, components, state management, and connecting to a backend API - all the concepts we've explored in this guide.
Read previous part
Setting Up Your Development Environment
Read next part
Building the Backend API with Node.js and Express
Enjoyed the read? Help us spread the word — say something nice!
Guide Parts
Enjoyed the read? Help us spread the word — say something nice!
Guide Parts