Authentication is the process of verifying who a user is. In a React application, authentication is commonly connected to a backend API that handles login, user accounts, sessions, and authentication tokens.
Authentication verifies the identity of a user.
For example, a login form may ask for:
The backend verifies the submitted credentials.
| Authentication | Authorization |
|---|---|
| Checks who the user is | Checks what the user is allowed to access |
| Usually happens during login | Usually happens after identity is established |
| Example: Login | Example: Accessing an admin page |
User
↓
Login Form
↓
React
↓
Backend API
↓
Verify Credentials
↓
Authentication Result
↓
React Application
A React login form can collect the user's credentials.
function Login() {
const [email, setEmail] =
useState("");
const [password, setPassword] =
useState("");
return (
<form>
<input
type="email"
value={email}
onChange={e =>
setEmail(
e.target.value
)
}
/>
<input
type="password"
value={password}
onChange={e =>
setPassword(
e.target.value
)
}
/>
</form>
);
}
The login form can send credentials to a backend API.
async function handleLogin(e) {
e.preventDefault();
const response = await fetch(
"/api/login",
{
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body: JSON.stringify({
email,
password
})
}
);
}
A backend may return information indicating whether authentication succeeded.
{
"user": {
"id": 101,
"name": "Rahul"
},
"token": "example-token"
}
The exact response structure depends on the backend API.
React can maintain information about the current authentication state.
const [user, setUser] =
useState(null);
A value of null can represent that no authenticated user is
currently available in the application's state.
After successful authentication, the application can update its user state.
setUser(responseData.user);
Components can then use this state to display user-specific information.
After a successful login, the application may navigate the user to another page.
if (response.ok) {
setUser(data.user);
navigate("/dashboard");
}
The exact navigation behavior depends on the application's routing design.
The application should provide feedback when authentication fails.
if (!response.ok) {
setError(
"Invalid email or password"
);
}
The backend should decide whether the submitted credentials are valid.
Login is asynchronous, so a loading state can prevent repeated submissions while the request is in progress.
const [loading, setLoading] =
useState(false);
Before sending the request:
setLoading(true);
async function handleLogin(e) {
e.preventDefault();
setLoading(true);
setError("");
try {
const response = await fetch(
"/api/login",
{
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body: JSON.stringify({
email,
password
})
}
);
if (!response.ok) {
throw new Error(
"Login failed"
);
}
const data =
await response.json();
setUser(data.user);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
}
Some authentication systems return a token after successful login.
{
"token": "example-token"
}
The client can use an authentication token when making later API requests, according to the backend's authentication design.
A common HTTP authentication pattern is the Bearer authentication scheme.
fetch("/api/profile", {
headers: {
Authorization:
`Bearer ${token}`
}
});
The backend should validate the token before returning protected data.
Applications need a strategy for maintaining authentication between requests. The strategy depends on the backend and security design.
Some applications use:
React Context can be used to make authentication state available to many components.
const AuthContext =
createContext(null);
An AuthProvider can then share the current user and authentication functions.
function AuthProvider({
children
}) {
const [user, setUser] =
useState(null);
return (
<AuthContext.Provider
value={{
user,
setUser
}}
>
{children}
</AuthContext.Provider>
);
}
function Profile() {
const {
user
} = useContext(
AuthContext
);
return (
<h2>
Welcome {user.name}
</h2>
);
}
Logout removes or invalidates the application's authenticated state according to the authentication system.
function logout() {
setUser(null);
navigate("/login");
}
For server-managed sessions or token-based systems, logout may also require a backend request or other server-side action.
Some authentication systems provide a logout endpoint.
async function logout() {
await fetch(
"/api/logout",
{
method: "POST"
}
);
setUser(null);
navigate("/login");
}
When an application starts, it may need to determine whether the current user has an active authenticated session.
useEffect(() => {
fetch("/api/me")
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error(
"Not authenticated"
);
})
.then(data => {
setUser(data);
})
.catch(() => {
setUser(null);
});
}, []);
When checking the current session, it can be useful to distinguish between "not authenticated" and "authentication status is still being checked."
const [authLoading, setAuthLoading] =
useState(true);
After the authentication check completes:
setAuthLoading(false);
Open Application
↓
Check Authentication
↓
Authenticated?
↙ ↘
Yes No
↓ ↓
Dashboard Login
↓
Protected Data
↓
Logout
↓
Login
Authentication state can work together with React Router to control which pages are available in the user interface.
<Routes>
<Route
path="/login"
element={<Login />}
/>
<Route
path="/dashboard"
element={<Dashboard />}
/>
</Routes>
Access control should ultimately be enforced by the backend, not only by hiding frontend routes.
An authentication context may contain more than just the current user.
const authValue = {
user,
loading,
login,
logout
};
This provides a central interface for authentication-related operations.
React
↓
Login Form
↓
Authentication API
↓
Backend
↓
Database / Session System
↓
Authentication Result
↓
React Auth State
↓
Application UI
import {
createContext,
useContext,
useState
} from "react";
const AuthContext =
createContext(null);
function AuthProvider({
children
}) {
const [user, setUser] =
useState(null);
async function login(
email,
password
) {
const response = await fetch(
"/api/login",
{
method: "POST",
headers: {
"Content-Type":
"application/json"
},
body: JSON.stringify({
email,
password
})
}
);
if (!response.ok) {
throw new Error(
"Login failed"
);
}
const data =
await response.json();
setUser(data.user);
}
function logout() {
setUser(null);
}
return (
<AuthContext.Provider
value={{
user,
login,
logout
}}
>
{children}
</AuthContext.Provider>
);
}
function Profile() {
const {
user,
logout
} = useContext(
AuthContext
);
if (!user) {
return (
<p>
Please log in.
</p>
);
}
return (
<div>
<h2>
Welcome {user.name}
</h2>
<button
onClick={logout}
>
Logout
</button>
</div>
);
}
export default AuthProvider;
import {
useState,
useContext
} from "react";
import {
useNavigate
} from "react-router-dom";
function Login() {
const {
login
} = useContext(
AuthContext
);
const navigate =
useNavigate();
const [email, setEmail] =
useState("");
const [password, setPassword] =
useState("");
const [error, setError] =
useState("");
const [loading, setLoading] =
useState(false);
async function handleSubmit(e) {
e.preventDefault();
setLoading(true);
setError("");
try {
await login(
email,
password
);
navigate("/dashboard");
} catch (err) {
setError(
err.message
);
} finally {
setLoading(false);
}
}
return (
<form
onSubmit={handleSubmit}
>
<input
type="email"
placeholder="Email"
value={email}
onChange={e =>
setEmail(
e.target.value
)
}
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={e =>
setPassword(
e.target.value
)
}
/>
{error && (
<p>
{error}
</p>
)}
<button
type="submit"
disabled={loading}
>
{loading
? "Logging in..."
: "Login"}
</button>
</form>
);
}
export default Login;
Login
↓
Backend Verification
↓
Authenticated User
↓
Auth State
↓
Application
↓
Logout
useState can store authentication state in a component.Question: What is the primary purpose of authentication?