Building Private Routes in React with Context, Hooks, and HOC

Mehul Kothari
10 min readJul 16, 2023

--

Creating private routes in a React application is a common requirement when dealing with authentication and authorization. Private routes restrict access to certain routes based on the user’s authentication status, ensuring that only authenticated users can access protected content. In this tutorial, we’ll explore three different approaches to implementing private routes in React using context, hooks, and Higher Order Components (HOC).

Github Link: https://github.com/mehulk05/react-auth

Prerequisites

To follow along with this tutorial, make sure you have the following dependencies installed in your React project:

  • React (v16.8 or above)
  • react-router-dom (v5 or above)

The Use Case for Private Routes

Private routes are commonly used in applications that require user authentication. They play a crucial role in protecting sensitive content, such as user profiles, dashboards, or admin panels, from unauthorized access. By implementing private routes, you can ensure that only authenticated users can access these protected areas of your application.

Private routes also provide a seamless user experience by automatically redirecting unauthenticated users to the login page when they try to access protected content. This helps to maintain the security and integrity of your application and prevents users from accessing unauthorized resources.

Approach 1: Context API

The first approach we’ll explore is using the Context API in React to manage the authentication state and provide the necessary authentication information to the components.

In this approach, we create an AuthContext using the createContext function from React. The AuthContext provides the authentication state, setter functions, and loading state using the useState hook. We simulate an asynchronous check for authentication in the useEffect hook, updating the authentication status based on the presence of a token in the localStorage.

To create a protected route, we use a ProtectedRoute component that checks the authentication status from the AuthContext using the useContext hook. If the user is authenticated, the component renders the specified component; otherwise, it redirects the user to the login route.

Step 1: Create the Auth Context

First, let’s create the AuthContext.js file, which will provide the authentication state and setter functions using the Context API.

AuthContext.js

// AuthContext.js
import React, { createContext, useState, useEffect } from 'react';

export const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
const [isAuthenticated, setIsAuthenticated] = useState(null); // null represents the initial loading state
const [loading, setLoading] = useState(false);

useEffect(() => {
// Simulate an asynchronous check for authentication
const checkAuthStatus = () => {
const token = localStorage.getItem('token');
setIsAuthenticated(!!token); // Update the authentication status based on the presence of a token
};

checkAuthStatus();
}, []);

return (
<AuthContext.Provider value={{ isAuthenticated, setIsAuthenticated, loading, setLoading }}>
{children}
</AuthContext.Provider>
);
};

In this code, we create the AuthContext using createContext from React. We also create an AuthProvider component that wraps the application and provides the authentication state, setter functions, and loading state using the useState hook. The useEffect hook simulates an asynchronous check for authentication by checking for the presence of a token in the localStorage. The authentication status is updated accordingly

Step 2: Implement the ProtectedRoute Component

Next, let’s create a ProtectedRoute.js file that defines a protected route component. This component renders the specified component if the user is authenticated; otherwise, it redirects to the login route.

// ProtectedRoute.js
import React, { useContext } from 'react';
import { Route, Redirect } from 'react-router-dom';
import { AuthContext } from '../context/AuthContext';

const ProtectedRoute = ({ component: Component, ...rest }) => {
const { isAuthenticated } = useContext(AuthContext);

return (
<Route
{...rest}
render={(props) =>
isAuthenticated ? (
<Component key={props.location.key} {...props} />
) : (
<Redirect to="/login" />
)
}
/>
);
};

export default ProtectedRoute;

In this component, we access the isAuthenticated value from the AuthContext using the useContext hook. If the user is authenticated, we render the specified component with a unique key. Otherwise, we redirect the user to the login route.

Step 3: Implement the Routes

Now, let’s modify our routes file (e.g., Routes.js) to include the private routes and the login route.

// Routes.js
import React, { useContext } from 'react';
import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
import { AuthContext } from '../context/AuthContext';
import ProtectedRoute from '../components/ProtectedRoute';
import Home from '../components/Home';
import AdminDashboard from '../components/AdminDashboard';
import Login from '../components/Login';

const Routes = () => {
const { loading } = useContext(AuthContext);

return (
<>
{loading ? (
<div>Loading...</div>
) : (
<Router>
<Switch>
<Route path="/login" component={Login} exact />
<ProtectedRoute path="/admin" component={AdminDashboard} exact />
<ProtectedRoute path="/home" component={Home} exact />
<Route exact path="/">
<Redirect to="/home" />
</Route>
</Switch>
</Router>
)}
</>
);
};

export default Routes;

In this file, we import the AuthContext and access the loading state. If the loading state is true, we display a loading message. Otherwise, we define the routes using Route and ProtectedRoute components. The ProtectedRoute components render the corresponding components only if the user is authenticated. The Route component with the path /login renders the login component. Finally, the Route component with the exact path / redirects to the /home route.

Step 4: Implement the Login Component

Let’s implement the login component, which will handle the user login functionality.

Login Component

// Login.js (Context API)

import React, { useState, useContext } from 'react';
import { useHistory } from 'react-router-dom';
import { AuthContext } from './AuthContext';

const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const history = useHistory();
const { isAuthenticated, setIsAuthenticated } = useContext(AuthContext);

const handleLogin = (e) => {
e.preventDefault();
setLoading(true);
// Simulate an asynchronous login process
setTimeout(() => {
// Perform login logic, e.g., validate email and password
// Assuming successful login for demonstration purposes
setIsAuthenticated(true);
setLoading(false);
history.push('/home');
}, 1500);
};

return (
<div>
<h1>Login</h1>
{loading ? (
<div>Loading...</div>
) : (
<form onSubmit={handleLogin}>
<div>
<label>Email:</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<label>Password:</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<button type="submit">Login</button>
</form>
)}
</div>
);
};

export default Login;

In this component, we use the useState hook to manage the email and password inputs' state. The useHistory hook from react-router-dom is used to programmatically navigate to the /home route after successful login.

The handleLogin function performs the login logic, such as validating the email and password. For demonstration purposes, we assume a successful login and set the isAuthenticated value in the AuthContext to true. Then, we use the history.push function to navigate to the /home route.

Approach 2: Hooks

The second approach to creating private routes is by using custom hooks. This approach provides a more flexible and reusable way to access the authentication state throughout the application.

Approach 2 leverages React hooks to manage the authentication state in a custom hook called useAuth. The useAuth hook is responsible for providing the authentication state, updating the authentication status, and managing the loading state.

Step 1: Create the useAuth Hook

Create a file called useAuth.js and define the useAuth custom hook that provides the authentication status and setter functions.

// useAuth.js (Hooks)

import { useState, useEffect } from 'react';

const useAuth = () => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [loading, setLoading] = useState(false);

useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
setIsAuthenticated(true);
}
}, []);

return { isAuthenticated, setIsAuthenticated, loading, setLoading };
};

export default useAuth;

Step 2: Modify the ProtectedRoute Component

In the ProtectedRoute.js component, we can now use the useAuth hook to access the authentication status.

// ProtectedRoute.js (Hooks)

import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import useAuth from './useAuth';

const ProtectedRoute = ({ component: Component, ...rest }) => {
const { isAuthenticated, loading } = useAuth();

return (
<Route
{...rest}
render={(props) =>
loading ? (
<div>Loading...</div>
) : isAuthenticated ? (
<Component {...props} />
) : (
<Redirect to="/login" />
)
}
/>
);
};

export default ProtectedRoute;

In this updated code, we import the useAuth hook and destructure the isAuthenticated and loading states from its return value. We then use these states in the render function of the Route component to conditionally render the appropriate component or redirect to the login page.

If loading is true, we display a loading message. If isAuthenticated is true, we render the Component. Otherwise, we redirect to the /login route.

Step 3: Update the Login Component

In the login component, we can use the useAuth hook to access the isAuthenticated value and the setIsAuthenticated setter function.

const { isAuthenticated, setIsAuthenticated, loading, setLoading } = useAuth();

const handleLogin = (e) => {
e.preventDefault();
setLoading(true);
// Simulate an asynchronous login process
setTimeout(() => {
// Perform login logic, e.g., validate email and password
// Assuming successful login for demonstration purposes
setIsAuthenticated(true);
setLoading(false);
history.push('/home');
}, 1500);
};

In this code, we import the useAuth hook and use it to access the isAuthenticated value, the setIsAuthenticated setter function, the loading state, and the setLoading setter function. The rest of the code remains the same.

Approach 3: Higher Order Component (HOC)

The third approach to creating private routes is by using Higher Order Components (HOC). HOCs are a pattern in React that allows us to wrap a component with additional functionality.

Approach 3 utilizes a Higher Order Component (HOC) called withAuth to handle the authentication logic. The withAuth HOC wraps a component and provides conditional rendering based on the authentication state.

Step 1: Create the withAuth HOC

Create a file called withAuth.js and define the withAuth

// withAuth.js (HOC)

import React, { useState } from 'react';
import { Redirect } from 'react-router-dom';

const withAuth = (Component) => {
return (props) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [loading, setLoading] = useState(false);

if (loading) {
return <div>Loading...</div>;
}

return isAuthenticated ? <Component {...props} /> : <Redirect to="/login" />;
};
};

export default withAuth;

In this code, we create the withAuth HOC that wraps the component and manages the isAuthenticated and loading states using the useState hook. If the loading state is true, the HOC renders a loading message. Otherwise, it renders the wrapped component if the user is authenticated or redirects to the login page if the user is not authenticated.

Step 2: Modify the ProtectedRoute Component

// ProtectedRoute.js (HOC)

import React from 'react';
import { Route } from 'react-router-dom';
import withAuth from './withAuth';

const ProtectedRoute = ({ component: Component, ...rest }) => {
const WrappedComponent = withAuth(Component);

return <Route {...rest} component={WrappedComponent} />;
};

export default ProtectedRoute;

In this code, we import the withAuth HOC and use it to wrap the specified component. The rest of the code remains the same.

Step 3: Update the Login Component


const Login = ({ isAuthenticated, loading, setLoading }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const history = useHistory();

const handleLogin = (e) => {
e.preventDefault();
setLoading(true); // Set the loading state to true
// Simulate an asynchronous login process
setTimeout(() => {
// Perform login logic, e.g., validate email and password
// Assuming successful login for demonstration purposes
setLoading(false); // Set the loading state to false
history.push('/home');
}, 1500);
};

// JSX

export default withAuth(Login);

In the updated Login component, we receive the isAuthenticated, loading, and setLoading props from the HOC. The loading and setLoading props are used to manage the loading state in the handleLogin function.

Ideal Use Cases and Advantages and Disadvantages

Each approach has its ideal use cases and potential disadvantages:

Context API:

  • Ideal Use Cases: Suitable for small to medium-sized applications where centralized state management is preferred. It works well when authentication is a global concern across the application.
  • Advantage: Centralized authentication state management using the AuthContext. Easy access to the authentication state and setter functions throughout the component tree.
  • Disadvantages: Requires additional setup and boilerplate code to create the AuthContext and Provider. May be less suitable for larger, complex applications that require fine-grained control over the authentication state.

Hooks:

  • Ideal Use Cases: Ideal for applications that require flexible and reusable authentication state management. It works well with functional components and can be easily integrated with existing hooks and side effects.
  • Advantage: Provides a more flexible and reusable way to access the authentication state using a custom hook. Can easily integrate with existing hooks and functional components.
  • Disadvantages: Requires custom hook creation and management, which may introduce complexity for developers who are unfamiliar with hooks. May require additional code to handle loading states and side effects.

Higher Order Component (HOC):

  • Ideal Use Cases: Works well with class components and existing HOC-based patterns. Suitable for applications where authentication logic needs to be encapsulated and shared across multiple components.
  • Advantage: Works well with class components and existing HOC-based patterns.
  • Disadvantages: Requires understanding of HOC concepts and usage, which may be challenging for developers new to React. Can become cumbersome when dealing with deeply nested components.

It’s important to consider the specific requirements and complexity of your application when choosing the most suitable approach for implementing private routes.

Conclusion:

In this tutorial, we explored three different approaches to implementing private routes in a React application: using the Context API, hooks, and Higher Order Components (HOC). Each approach has its advantages and considerations based on the specific requirements of your application.

The Context API approach provides centralized authentication state management through the AuthContext. It offers easy access to the authentication state and setter functions throughout the component tree. However, it requires additional setup and may be less suitable for larger applications.

Using custom hooks offers flexibility and reusability for authentication state management. It integrates well with functional components and allows granular control over the authentication state and loading state. However, it requires custom hook creation and management, which can be challenging for developers new to hooks.

Higher Order Components (HOCs) provide a way to encapsulate authentication logic and easily wrap components with additional functionality. They work well with class components and existing HOC patterns. However, understanding and using HOCs effectively may require familiarity with HOC concepts and usage.

When choosing the ideal approach, consider the size and complexity of your application, as well as your familiarity with the different concepts and patterns. Context API is suitable for smaller applications where centralized state management is preferred, hooks provide flexibility and reusability, and HOCs work well for encapsulating authentication logic.

By implementing private routes, you can protect sensitive content, provide a seamless user experience, and maintain the security and integrity of your React application. Choose the approach that best fits your application’s needs and development preferences.

Read More about my blogs here

--

--

Mehul Kothari

Mean stack and Full stack Developer. Open to work as freelancer for designing and developing websites.