Step-by-Step Guide to Creating an Authentication App in React with Typescript and Vite
Table of contents
I'll provide you with a step-by-step guide to creating a User Authentication App with the features you've specified using React, Vite, MUI, Tailwind CSS, and TypeScript. We'll set up the project and implement the required features.
- Project Setup
First, let's create a new Vite project with React and TypeScript:
npm create vite@latest auth-app -- --template react-ts
cd auth-app
npm install
Now, let's install the required dependencies:
npm install @mui/material @emotion/react @emotion/styled
npm install tailwindcss postcss autoprefixer @tailwindcss/nesting
npm install react-router-dom formik yup axios
Configure Tailwind CSS and MUI
First, let's set up Tailwind CSS. Create a
tailwind.config.mjs
file in the root directory and add the following content:import tailwindNesting from '@tailwindcss/nesting'; export default { content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}", ], theme: { extend: {}, }, plugins: [ tailwindNesting(), ], };
Next, create a
postcss.config.mjs
file in the root directory and add the following content:import tailwindcss from 'tailwindcss'; import autoprefixer from 'autoprefixer'; export default { plugins: [ tailwindcss, autoprefixer, ], };
If you're using Vite, you might need to update your
vite.config.ts
file to use ES modules as well:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
});
- custom theme
Create a src/theme.ts
file:
import { createTheme } from '@mui/material/styles';
const theme = createTheme({
palette: {
primary: {
main: '#3f51b5',
},
secondary: {
main: '#f50057',
},
},
typography: {
fontFamily: 'Roboto, Arial, sans-serif',
},
});
export default theme;
Context API
implementing these additional features to complete your User Authentication App. We'll use Context API for state management and add API calls for registration and login.
First, let's create a custom error class in
src/utils/AuthError.ts
export class AuthError extends Error { constructor(public code: string, message: string) { super(message); this.name = 'AuthError'; } }
Set up API calls and state management
First, let's create an AuthContext and AuthProvider. Create a new file
src/contexts/AuthContext.tsx
:
import React, { createContext, useState, useContext, ReactNode } from "react";
import axios, { AxiosError } from "axios";
import { AuthError } from "../utils/AuthError";
interface User {
username: string;
firstname: string;
lastname: string;
emailId: string;
}
interface AuthContextType {
user: User | null;
login: (username: string, password: string) => Promise<void>;
register: (userData: User & { password: string }) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const [user, setUser] = useState<User | null>(null);
const handleApiError = (error: unknown): never => {
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError<{ message: string }>;
if (axiosError.response) {
throw new AuthError(
axiosError.response.status.toString(),
axiosError.response.data.message ||
"An error occurred during authentication"
);
}
}
throw new AuthError("UNKNOWN", "An unknown error occurred");
};
const login = async (username: string, password: string) => {
try {
const response = await axios.post("https://reqres.in/api/login", {
email: username,
password: password,
});
const userData = response.data;
setUser(userData);
localStorage.setItem("token", userData.token);
} catch (error) {
handleApiError(error);
}
};
const register = async (userData: User & { password: string }) => {
try {
const response = await axios.post("https://api.example.com/register", {
email: userData.emailId,
password: userData.password,
});
const newUser = response.data;
setUser(newUser);
localStorage.setItem("token", newUser.token);
} catch (error) {
handleApiError(error);
}
};
const logout = () => {
setUser(null);
localStorage.removeItem("token");
};
return (
<AuthContext.Provider value={{ user, login, register, logout }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};
First, let's create an AuthContext and AuthProvider. Create a new file
src/contexts/AuthContext.tsx
:Let's create the necessary components for our app.
src/components/Register.tsx
:
import React, { useState } from "react";
import { useFormik } from "formik";
import * as Yup from "yup";
import { TextField, Button, Box, Typography, Alert } from "@mui/material";
import { useAuth } from "../../contexts/AuthContext";
import { useNavigate } from "react-router-dom";
import { AuthError } from "../../utils/AuthError";
const Register: React.FC = () => {
const { register } = useAuth();
const navigate = useNavigate();
const [error, setError] = useState<string | null>(null);
const formik = useFormik({
initialValues: {
username: "",
password: "",
firstname: "",
lastname: "",
emailId: "",
},
validationSchema: Yup.object({
username: Yup.string().required("Required"),
password: Yup.string()
.min(6, "Must be at least 6 characters")
.required("Required"),
firstname: Yup.string().required("Required"),
lastname: Yup.string().required("Required"),
emailId: Yup.string().email("Invalid email address").required("Required"),
}),
onSubmit: async (values, { setSubmitting }) => {
try {
await register(values);
navigate("/profile");
} catch (error) {
if (error instanceof AuthError) {
switch (error.code) {
case "409":
setError("Username or email already exists");
break;
case "400":
setError("Invalid input. Please check your information");
break;
default:
setError(error.message);
}
} else {
setError("An unexpected error occurred. Please try again");
}
} finally {
setSubmitting(false);
}
},
});
return (
<Box className="max-w-md mx-auto mt-8 p-6 bg-white rounded shadow-md">
<Typography variant="h4" component="h1" gutterBottom>
Register
</Typography>
{error && (
<Alert severity="error" className="mb-4">
{error}
</Alert>
)}
<form onSubmit={formik.handleSubmit}>
<TextField
fullWidth
id="username"
name="username"
label="Username"
value={formik.values.username}
onChange={formik.handleChange}
error={formik.touched.username && Boolean(formik.errors.username)}
helperText={formik.touched.username && formik.errors.username}
className="mb-4"
/>
<TextField
fullWidth
id="password"
name="password"
label="Password"
type="password"
value={formik.values.password}
onChange={formik.handleChange}
error={formik.touched.password && Boolean(formik.errors.password)}
helperText={formik.touched.password && formik.errors.password}
className="mb-4"
/>
<TextField
fullWidth
id="firstname"
name="firstname"
label="First Name"
value={formik.values.firstname}
onChange={formik.handleChange}
error={formik.touched.firstname && Boolean(formik.errors.firstname)}
helperText={formik.touched.firstname && formik.errors.firstname}
className="mb-4"
/>
<TextField
fullWidth
id="lastname"
name="lastname"
label="Last Name"
value={formik.values.lastname}
onChange={formik.handleChange}
error={formik.touched.lastname && Boolean(formik.errors.lastname)}
helperText={formik.touched.lastname && formik.errors.lastname}
className="mb-4"
/>
<TextField
fullWidth
id="emailId"
name="emailId"
label="Email"
value={formik.values.emailId}
onChange={formik.handleChange}
error={formik.touched.emailId && Boolean(formik.errors.emailId)}
helperText={formik.touched.emailId && formik.errors.emailId}
className="mb-4"
/>
<Button
color="primary"
variant="contained"
fullWidth
type="submit"
disabled={formik.isSubmitting}
>
{formik.isSubmitting ? "Registering..." : "Register"}
</Button>
</form>
</Box>
);
};
export default Register;
src/components/Login.tsx
:
import React, { useState } from "react";
import { useFormik } from "formik";
import * as Yup from "yup";
import { TextField, Button, Box, Typography, Alert } from "@mui/material";
import { useAuth } from "../../contexts/AuthContext";
import { useNavigate } from "react-router-dom";
import { AuthError } from "../../utils/AuthError";
const Login: React.FC = () => {
const { login } = useAuth();
const navigate = useNavigate();
const [error, setError] = useState<string | null>(null);
const formik = useFormik({
initialValues: {
username: "",
password: "",
},
validationSchema: Yup.object({
username: Yup.string().required("Required"),
password: Yup.string().required("Required"),
}),
onSubmit: async (values, { setSubmitting }) => {
try {
await login(values.username, values.password);
navigate("/profile");
} catch (error) {
if (error instanceof AuthError) {
switch (error.code) {
case "401":
setError("Invalid username or password");
break;
case "429":
setError("Too many login attempts. Please try again later");
break;
default:
setError(error.message);
}
} else {
setError("An unexpected error occurred. Please try again");
}
} finally {
setSubmitting(false);
}
},
});
return (
<Box className="max-w-md mx-auto mt-8 p-6 bg-white rounded shadow-md">
<Typography variant="h4" component="h1" gutterBottom>
Login
</Typography>
{error && (
<Alert severity="error" className="mb-4">
{error}
</Alert>
)}
<form onSubmit={formik.handleSubmit}>
<TextField
fullWidth
id="username"
name="username"
label="Username"
value={formik.values.username}
onChange={formik.handleChange}
error={formik.touched.username && Boolean(formik.errors.username)}
helperText={formik.touched.username && formik.errors.username}
className="mb-4"
/>
<TextField
fullWidth
id="password"
name="password"
label="Password"
type="password"
value={formik.values.password}
onChange={formik.handleChange}
error={formik.touched.password && Boolean(formik.errors.password)}
helperText={formik.touched.password && formik.errors.password}
className="mb-4"
/>
<Button
color="primary"
variant="contained"
fullWidth
type="submit"
disabled={formik.isSubmitting}
>
{formik.isSubmitting ? "Logging in..." : "Login"}
</Button>
</form>
</Box>
);
};
export default Login;
src/components/Profile.tsx
:import React from "react"; import { Box, Typography, Button } from "@mui/material"; import { useAuth } from "../../contexts/AuthContext"; import { useNavigate } from "react-router-dom"; const Profile: React.FC = () => { const { user, logout } = useAuth(); const navigate = useNavigate(); const handleLogout = () => { logout(); navigate("/login"); }; if (!user) { return <Typography>Loading...</Typography>; } return ( <Box className="max-w-md mx-auto mt-8 p-6 bg-white rounded shadow-md"> <Typography variant="h4" component="h1" gutterBottom> User Profile </Typography> <Typography variant="body1">Username: {user.username}</Typography> <Typography variant="body1">First Name: {user.firstname}</Typography> <Typography variant="body1">Last Name: {user.lastname}</Typography> <Typography variant="body1">Email: {user.emailId}</Typography> <Button color="secondary" variant="contained" onClick={handleLogout} className="mt-4" > Logout </Button> </Box> ); }; export default Profile;
- Set up Routing and Protected Routes
Create a src/components/ProtectedRoute.tsx
file
import React from "react";
import { Navigate } from "react-router-dom";
import { useAuth } from "../../contexts/AuthContext";
interface ProtectedRouteProps {
children: React.ReactNode;
}
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
const { user } = useAuth();
return user ? <>{children}</> : <Navigate to="/login" />;
};
export default ProtectedRoute;
- Update App.tsx
Update your src/App.tsx
file:
import React from "react";
import { BrowserRouter as Router, Route, Routes, Link } from "react-router-dom";
import { ThemeProvider } from "@mui/material/styles";
import { CssBaseline, AppBar, Toolbar, Button } from "@mui/material";
import theme from "./theme";
import Register from "./components/auth/Register";
import Login from "./components/auth/Login";
import Profile from "./components/auth/Profile";
import ProtectedRoute from "./components/auth/ProtectedRoute";
import { AuthProvider } from "./contexts/AuthContext";
const App: React.FC = () => {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<AuthProvider>
<Router>
<AppBar position="static">
<Toolbar>
<Button color="inherit" component={Link} to="/register">
Register
</Button>
<Button color="inherit" component={Link} to="/login">
Login
</Button>
<Button color="inherit" component={Link} to="/profile">
Profile
</Button>
</Toolbar>
</AppBar>
<Routes>
<Route path="/register" element={<Register />} />
<Route path="/login" element={<Login />} />
<Route
path="/profile"
element={
<ProtectedRoute>
<Profile />
</ProtectedRoute>
}
/>
</Routes>
</Router>
</AuthProvider>
</ThemeProvider>
);
};
export default App;
- Update index.css
Update your src/index.css
file to include Tailwind CSS:
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
Update your src/index.css
file to include Tailwind CSS:
Subscribe to my newsletter
Read articles from Arunkumar Gopalan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by