background

Building the React Frontend

Last updated September 18, 2023

JavaScript

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:

  1. Register a new account
  2. Login with your credentials
  3. Create, view, update, and delete tasks
  4. Logout of the application

Deployment Considerations

When deploying your full-stack application to production, consider:

  1. Environment Variables: Update the .env files for both backend and frontend
  2. Build Process: Create a production build with npm run build
  3. API Endpoints: Ensure API URLs are correct for production
  4. CORS Configuration: Update CORS settings on the backend
  5. Database Connection: Set up a production database
  6. Authentication: Ensure secure JWT secret and HTTPs
  7. Error Handling: Implement comprehensive error handling

Next Steps

To enhance your application, consider:

  1. Task Filtering & Sorting: Add filters by status, priority, and date
  2. Pagination: Implement pagination for large task lists
  3. Search Functionality: Add search capability
  4. Notifications: Add notification systems for due dates
  5. Team Collaboration: Allow sharing tasks between users
  6. 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:

Everything Is AWESOME

This comprehensive video covers React fundamentals, components, state management, and connecting to a backend API - all the concepts we've explored in this guide.

Enjoyed the read? Help us spread the word — say something nice!

Guide Parts

logo
© 2025 Guidely. All rights reserved.