How to Secure useNavigate Hook in React

Securing navigation in a React application is crucial for protecting sensitive routes and ensuring that unauthorized users cannot access restricted pages. While the useNavigate hook from React Router is a client-side tool that updates the browser history, it must be combined with authentication states and route guards to be truly secure. This article explains how to secure the useNavigate hook by implementing protected routes, creating a custom secure navigation hook, and validating user permissions before redirecting.

Why You Need to Secure useNavigate

In a single-page application (SPA), client-side routing is inherently insecure on its own. A tech-savvy user can easily manipulate the application state or manually type a URL to bypass client-side checks.

Therefore, “securing” useNavigate requires a two-pronged approach: 1. Preventing unauthorized navigation attempts in the UI. 2. Protecting the destination routes so that even if a user manually navigates to a URL, they are blocked and redirected.


Method 1: Protecting the Destination Routes (Route Guards)

The most robust way to secure navigation is to protect the routes themselves. If a route is protected, any attempt to use useNavigate to reach it will fail unless the user meets the authentication criteria.

Here is how to create a ProtectedRoute wrapper component:

import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from './context/AuthContext';

const ProtectedRoute = ({ allowedRoles }) => {
  const { isAuthenticated, userRole } = useAuth();

  if (!isAuthenticated) {
    // Redirect unauthorized users to login
    return <Navigate to="/login" replace />;
  }

  if (allowedRoles && !allowedRoles.includes(userRole)) {
    // Redirect users who do not have the correct role
    return <Navigate to="/unauthorized" replace />;
  }

  return <Outlet />;
};

export default ProtectedRoute;

You then wrap your secure routes inside this component in your router configuration:

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import ProtectedRoute from './ProtectedRoute';
import AdminDashboard from './AdminDashboard';
import Login from './Login';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/login" element={<Login />} />
        
        {/* Only Admins can access this route */}
        <Route element={<ProtectedRoute allowedRoles={['admin']} />}>
          <Route path="/admin" element={<AdminDashboard />} />
        </Route>
      </Routes>
    </Router>
  );
}

Method 2: Creating a Custom Secure Navigation Hook

Instead of calling useNavigate directly in your components, you can create a custom wrapper hook called useSecureNavigate. This hook intercepts the navigation request, checks the user’s permissions, and only executes the navigation if the user is authorized.

import { useNavigate } from 'react-router-dom';
import { useAuth } from './context/AuthContext';

export const useSecureNavigate = () => {
  const navigate = useNavigate();
  const { isAuthenticated, userRole } = useAuth();

  const secureNavigate = (to, requiredRoles = []) => {
    if (!isAuthenticated) {
      navigate('/login');
      return;
    }

    if (requiredRoles.length > 0 && !requiredRoles.includes(userRole)) {
      navigate('/unauthorized');
      return;
    }

    navigate(to);
  };

  return secureNavigate;
};

How to use the custom hook:

import { useSecureNavigate } from './hooks/useSecureNavigate';

const DashboardMenu = () => {
  const secureNavigate = useSecureNavigate();

  return (
    <div>
      {/* This will redirect to /login or /unauthorized if the user is not an admin */}
      <button onClick={() => secureNavigate('/admin', ['admin'])}>
        Go to Admin Panel
      </button>
    </div>
  );
};

Method 3: Conditional Inline Navigation

For simple scenarios, you can perform inline checks before calling the standard useNavigate hook. This is useful for handling single actions, such as form submissions that redirect based on the response.

import { useNavigate } from 'react-router-dom';
import { useAuth } from './context/AuthContext';

const SettingsButton = () => {
  const navigate = useNavigate();
  const { isAuthenticated } = useAuth();

  const handleNavigation = () => {
    if (isAuthenticated) {
      navigate('/settings');
    } else {
      navigate('/login', { state: { from: '/settings' } });
    }
  };

  return <button onClick={handleNavigation}>Settings</button>;
};

Summary of Best Practices