Skip to main content

Overview

Authentication in React applications typically involves managing user tokens, protecting routes, and coordinating authentication state across the application.
This guide covers token-based authentication using localStorage, React Router actions, and route protection strategies.

Authentication Form

Create a flexible authentication form supporting both login and signup:
import {
  Form,
  Link,
  useSearchParams,
  useActionData,
  useNavigation,
} from 'react-router-dom';

function AuthForm() {
  const data = useActionData();
  const navigation = useNavigation();
  const [searchParams] = useSearchParams();
  
  const isLogin = searchParams.get('mode') === 'login';
  const isSubmitting = navigation.state === 'submitting';
  
  return (
    <Form method="post">
      <h1>{isLogin ? 'Log in' : 'Create a new user'}</h1>
      
      {data && data.errors && (
        <ul>
          {Object.values(data.errors).map((err) => (
            <li key={err}>{err}</li>
          ))}
        </ul>
      )}
      
      {data && data.message && <p>{data.message}</p>}
      
      <p>
        <label htmlFor="email">Email</label>
        <input id="email" type="email" name="email" required />
      </p>
      
      <p>
        <label htmlFor="password">Password</label>
        <input id="password" type="password" name="password" required />
      </p>
      
      <div>
        <Link to={`?mode=${isLogin ? 'signup' : 'login'}`}>
          {isLogin ? 'Create new user' : 'Login'}
        </Link>
        <button disabled={isSubmitting}>
          {isSubmitting ? 'Submitting...' : 'Save'}
        </button>
      </div>
    </Form>
  );
}

export default AuthForm;
Use query parameters (?mode=login or ?mode=signup) to toggle between login and signup modes on the same page.

Authentication Action

Handle authentication requests using React Router actions:
import { json, redirect } from 'react-router-dom';

export async function action({ request }) {
  const searchParams = new URL(request.url).searchParams;
  const mode = searchParams.get('mode') || 'login';
  
  if (mode !== 'login' && mode !== 'signup') {
    throw json({ message: 'Unsupported mode.' }, { status: 422 });
  }
  
  const data = await request.formData();
  const authData = {
    email: data.get('email'),
    password: data.get('password'),
  };
  
  const response = await fetch('http://localhost:8080/' + mode, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(authData),
  });
  
  if (response.status === 422 || response.status === 401) {
    return response;
  }
  
  if (!response.ok) {
    throw json({ message: 'Could not authenticate user.' }, { status: 500 });
  }
  
  const resData = await response.json();
  const token = resData.token;
  
  localStorage.setItem('token', token);
  
  return redirect('/');
}
This example uses localStorage for token storage. For production applications, consider security implications and potentially use httpOnly cookies for sensitive tokens.

Token Management

Create utility functions to manage authentication tokens:
// util/auth.js
import { redirect } from 'react-router-dom';

export function getAuthToken() {
  const token = localStorage.getItem('token');
  return token;
}

export function tokenLoader() {
  return getAuthToken();
}

export function checkAuthLoader() {
  const token = getAuthToken();
  
  if (!token) {
    return redirect('/auth');
  }
  
  return null;
}
These loader functions can be attached to routes to provide authentication checks and data.

Route Protection

Protect routes by adding authentication loaders:
import { checkAuthLoader } from './util/auth';

const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    children: [
      {
        path: 'events/new',
        element: <NewEventPage />,
        loader: checkAuthLoader, // Protect this route
        action: newEventAction,
      },
      {
        path: 'events/:eventId/edit',
        element: <EditEventPage />,
        loader: checkAuthLoader, // Protect this route
        action: editEventAction,
      },
    ],
  },
]);

Conditional UI Rendering

Show/hide UI elements based on authentication state:
import { useRouteLoaderData } from 'react-router-dom';

function MainNavigation() {
  const token = useRouteLoaderData('root');
  
  return (
    <nav>
      <ul>
        <li><Link to="/">Home</Link></li>
        <li><Link to="/events">Events</Link></li>
        
        {token && (
          <>
            <li><Link to="/events/new">New Event</Link></li>
            <li>
              <Form method="post" action="/logout">
                <button>Logout</button>
              </Form>
            </li>
          </>
        )}
        
        {!token && (
          <li><Link to="/auth?mode=login">Login</Link></li>
        )}
      </ul>
    </nav>
  );
}
useRouteLoaderData allows you to access loader data from parent routes using their route ID.

Logout Implementation

Implement logout functionality:
// pages/Logout.js
import { redirect } from 'react-router-dom';

export function action() {
  localStorage.removeItem('token');
  return redirect('/');
}

Authenticated API Requests

Include authentication tokens in API requests:
import { getAuthToken } from './auth';

export async function createEvent(eventData) {
  const token = getAuthToken();
  
  const response = await fetch('http://localhost:8080/events', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + token,
    },
    body: JSON.stringify(eventData),
  });
  
  if (!response.ok) {
    throw new Error('Failed to create event');
  }
  
  return response.json();
}
Include the token in the Authorization header as a Bearer token for API authentication.

Token Expiration

Handle token expiration and automatic logout:
export function getTokenDuration() {
  const storedExpirationDate = localStorage.getItem('expiration');
  const expirationDate = new Date(storedExpirationDate);
  const now = new Date();
  const duration = expirationDate.getTime() - now.getTime();
  
  return duration;
}

export function getAuthToken() {
  const token = localStorage.getItem('token');
  
  if (!token) {
    return null;
  }
  
  const tokenDuration = getTokenDuration();
  
  if (tokenDuration < 0) {
    return 'EXPIRED';
  }
  
  return token;
}

export function tokenLoader() {
  const token = getAuthToken();
  
  if (token === 'EXPIRED') {
    localStorage.removeItem('token');
    localStorage.removeItem('expiration');
    return null;
  }
  
  return token;
}

Auto-Logout Timer

Implement automatic logout when token expires:
import { useEffect } from 'react';
import { useNavigate, useLoaderData, useSubmit } from 'react-router-dom';
import { getTokenDuration } from './util/auth';

function App() {
  const token = useLoaderData();
  const submit = useSubmit();
  
  useEffect(() => {
    if (!token) {
      return;
    }
    
    if (token === 'EXPIRED') {
      submit(null, { action: '/logout', method: 'post' });
      return;
    }
    
    const tokenDuration = getTokenDuration();
    
    const logoutTimer = setTimeout(() => {
      submit(null, { action: '/logout', method: 'post' });
    }, tokenDuration);
    
    return () => {
      clearTimeout(logoutTimer);
    };
  }, [token, submit]);
  
  return <RouterProvider router={router} />;
}

Security Best Practices

Development: localStorage is convenient for development and non-sensitive applications.Production: Consider using httpOnly cookies for storing authentication tokens to prevent XSS attacks:
// Server sets httpOnly cookie
res.cookie('token', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
  maxAge: 3600000,
});
Always validate tokens on the server side:
// Server-side validation
const isValidToken = await verifyJWT(token);

if (!isValidToken) {
  return res.status(401).json({ message: 'Invalid token' });
}
Handle authentication errors gracefully:
if (response.status === 401) {
  // Token expired or invalid
  localStorage.removeItem('token');
  return redirect('/auth?error=session-expired');
}

Common Patterns

Persistent Login

Store token expiration time:
const expirationTime = new Date();
expirationTime.setHours(
  expirationTime.getHours() + 1
);
localStorage.setItem(
  'expiration',
  expirationTime.toISOString()
);

Refresh Tokens

Implement token refresh mechanism:
async function refreshToken() {
  const response = await fetch(
    '/api/refresh',
    {
      method: 'POST',
      credentials: 'include',
    }
  );
  return response.json();
}

Routing

Learn about React Router setup and navigation

Deployment

Deploy authenticated applications securely

Build docs developers (and LLMs) love