loading...

Protected Routes with React Function Components

mychal profile image mychal ・11 min read

React Hooks + Router

Protected routes allow us to ensure only logged in users can access certain parts of our site that may contain private user information. In this post, we'll look at one way of implementing protected routes in React using function components along with react-router. We'll do this first with the useState hook, but in a subsequent post we'll also see how this can be achieved with React's Context API, which is a built-in solution for giving nested child components access to our application's state without the need to pass props all the down our component tree, a practice often referred to as prop drilling.

Getting Started

Let's start by creating a new React project. I'll be using the Create-React-App utility to automatically generate a baseline React app without needing to write any of the boilerplate configuration.

I'm using yarn but you can use npx if you prefer. Let's run the following command in our terminal:

yarn create react-app protected-routes

cd into the newly created protected-routes folder and run yarn start (or npm start) to fire up the dev server. It should open up your browser and display the React logo.

Create React App default page

React Router

So now that we have a basic React app, let's create a new directory called components inside the src directory. We'll create 2 components inside this directory: Landing.js and Dashboard.js

protected-routes/src/components/Landing.js
import React from 'react';
import { Link } from 'react-router-dom';

const Landing = () => {
  return (
    <div>
      <h1>Landing</h1>
      <p><Link to='/dashboard'>View Dashboard</Link></p>
      <button>Log In</button>
    </div>
  )
};

export default Landing;

Note, we're using the <Link> component from react-router instead of an anchor tag so that our app won't reload the page when the user clicks the link.

protected-routes/src/components/Dashboard.js
import React from 'react';

const Dashboard = () => {
  return (
    <div>
      <h1>Dashboard</h1>
      <p>Secret Page</p>
      <button>Log Out</button>
    </div>
  )
};

export default Dashboard;

We want to render each of these components depending on the route we're on. To do so, let's install react-router-dom.

yarn add react-router-dom

Open up the App.js file under the src directory and import in the following components from our newly installed package right after the CSS import. Note, I'm giving BrowserRouter the alias Router for brevity. Let's also import the 2 components we created.

protected-routes/src/App.js
import { BrowserRouter as Router, Route } from 'react-router-dom';
import Landing from './components/Landing';
import Dashboard from './components/Dashboard';

We can delete all the stock HTML inside the return statement besides the top level <div> and replace it with our own. We'll use the Route component we just imported to specify which component corresponds to which route. Lastly, we'll need to wrap the <Route> components with our <Router> (aka BrowserRouter) component to tie it all together. Our App.js file should look like this:

import React from 'react';
import logo from './logo.svg';
import './App.css';

import { BrowserRouter as Router, Route } from 'react-router-dom';
import Landing from './components/Landing';
import Dashboard from './components/Dashboard';

function App() {
  return (
    <div className="App">
      <Router>
        <Route exact path='/' component={Landing} />
        <Route exact path='/dashboard' component={Dashboard} />
      </Router>
    </div>
  );
}

export default App;

Now, when we visit localhost:3000 in the browser, we should see a rather plain page with the headline "Landing", a link to /dashboard, and a button for "Log In" (which doesn't do anything yet). Click the link and we'll see that the page now renders the Dashboard component, since our route has now changed to localhost:3000/dashboard

Click on link to /dashboard

With react-router, there's no need to write additional conditional-rendering logic or utilize state to keep track of which component we should be displaying. Pretty cool, right? 😎. But we still have an issue: our secret Dashboard page is accessible to anyone and everyone. How we can allow only the people who are authorized to view the page navigate to it? First, we'll need to keep track of whether or not our user is logged in our not. Let's see how we can do that using the useState hook.

useState Hook

Prior to the introduction of hooks in version 16.8, the only way to have stateful components in React was through classes. As it's name implies, the useState hook allows us to use state inside a function component. Let's implement useState to keep track of our logged in status.

In App.js, import useState using destructuring in the same line we import React.

protected-routes/src/App.js
import React, { useState } from 'react';

Next, inside of our App function and right before our return block, let's use array destructuring to create a user and setUser variables, which are the first and second elements that useState returns to us, respectively. We'll pass in an initial state of false, to indicate that we are not logged in when we first visit the page.

We'll also create a function called handleLogin which will invoke setUser and flip the user value to true when we click "Log In".

function App() {
  const [user, setUser] = useState(false);

  const handleLogin = e => {
    e.preventDefault();
    setUser(true);
  }

We need to pass this handleLogin function to our Landing component, but it won't work with our current set up since we're passing Landing in as a component prop to Route. We'll need to change the prop from component to render and pass it in as a function that returns our Landing component. Trust me, it sounds more confusing than it is, but in case you want to read more about it, feel free to check out this article.

Our App.js should look like this:

protected-routes/src/App.js
import React, { useState } from 'react';
import './App.css';

import { BrowserRouter as Router, Route } from 'react-router-dom';
import Landing from './components/Landing';
import Dashboard from './components/Dashboard';

function App() {
  const [user, setUser] = useState(false)

  const handleLogin = e => {
    e.preventDefault();
    setUser(true);
  }

  return (
    <div className="App">
      <Router>
        <Route exact path='/' handleLogin={handleLogin} render={props => <Landing {...props} user={user.toString()} handleLogin={handleLogin} />} />
        <Route exact path='/dashboard' component={Dashboard} />
      </Router>
    </div>
  );
}

export default App;

Note, I'm passing in user as a string so we can display it in our Landing component. If you have the React Developer Tools Chrome extension, you can use that to inspect the app's state and make sure everything is working properly.

Let's add an onClick handler to our button in the Landing component using the handleLogin function we just passed down as a prop. Remember to pass in props as an argument, in order to access it inside our component.

protected-routes/src/components/Landing.js
import React from 'react';
import { Link } from 'react-router-dom';

const Landing = props => {
  return (
    <div>
      <h1>Landing</h1>
      <p><Link to='/dashboard'>View Dashboard</Link></p>
      <p>Logged in status: {props.user}</p>
      <button onClick={props.handleLogin}>Log In</button>
    </div>
  )
};

export default Landing;

We should now be able to click the log in button and see our status change to true. This is our state being toggled.

changing log in status

Great, we have our logged in status. Don't worry about wiring up the Log Out button in Dashboard.js for now, we'll do that in the next section.

Now we need a way to allow a user to visit the Dashboard component only if their logged in status is true. How can we enforce that? Enter the protected route component.

Protected Routes

We'll create a new file called ProtectedRoute.js within the components directory. At a high-level, this component will act as a wrapper over react-router's Route component, ultimately returning out the component we wish to render. In other words, we're passing in the component we want to render through an intermediary that abstracts away the need to keep track of state inside our <Dashboard> component. In our case, <ProtectedRoute> becomes a higher-order component. It will be responsible for checking if we're logged in before rendering the <Dashboard> compoent, otherwise it will redirect users to another page (which we'll create shortly).

protected-routes/src/components/ProtectedRoute.js
import React from 'react';
import { Route } from 'react-router-dom';

const ProtectedRoute = ({ component: Component, ...rest }) => {
  return (
    <Route {...rest} render={
      props => <Component {...rest} {...props} />
    } />
  )
}

export default ProtectedRoute;

ProtectedRoute takes in all the same props that we had previously passed into our Route component and returns the very same Route component using the render prop. Let's step through each line individually:

const ProtectedRoute = ({ component: Component, ...rest }) => {
  • Here we're passing in an object which contains all the props that we'll pass in when we call our <ProtectedRoute> component from App.js. We specify Component so we can reference it later in our render prop. We use rest syntax to pass in our other props without having to know or list them individually.
return (<Route {...rest} render={ props => <Component {...rest} {...props} />
  • We're simply returning react-router's <Route> component and using its render prop to render the Component we passed in as an argument. We pass in the ...rest props from before in addition to the default props that <Route> provides normally.

We'll see how we can add logic here to check whether or not we're logged in. First let's make sure we haven't broken anything.

Open up App.js, import the ProtectedRoute component, and replace Route with ProtectedRoute where we specify the /dashboard route. Your return should look like this:

protected-routes/src/App.js
  return (
    <div className="App">
      <Router>
        <Route exact path='/' handleLogin={handleLogin} render={
          props => <Landing {...props} user={user.toString()} 
          handleLogin={handleLogin} />} />
        <ProtectedRoute exact path='/dashboard' component={Dashboard} />
      </Router>
    </div>
  );

Fingers crossed, it should work exactly the same. Now let's go back and fix the log out button before we add the logic to our ProtectedRoute.

In App.js, create a handleLogout route that looks identical to the handleLogin route except that it toggles our user state to false. Then pass it down a prop to our ProtectedRoute component. Our full file now looks like this:

protected-routes/src/App.js
import React, { useState } from 'react';
import './App.css';

import { BrowserRouter as Router, Route } from 'react-router-dom';
import Landing from './components/Landing';
import Dashboard from './components/Dashboard';
import ProtectedRoute from './components/ProtectedRoute';

function App() {
  const [user, setUser] = useState(false)

  const handleLogin = e => {
    e.preventDefault();
    setUser(true);
  }

  const handleLogout = e => {
    e.preventDefault();
    setUser(false);
  }

  return (
    <div className="App">
      <Router>
        <Route exact path='/' handleLogin={handleLogin} render={
          props => <Landing {...props} user={user.toString()} handleLogin={handleLogin} />} />
        <ProtectedRoute exact path='/dashboard' handleLogout={handleLogout} component={Dashboard} />
      </Router>
    </div>
  );
}

export default App;

Open up Dashboard.js and add an onClick handler that will trigger our handleLogout function when we click the Log Out button. Remember to pass in a props argument to our Dashboard function where we previously had empty parentheses.

<button onClick={props.handleLogout}>Log Out</button>

Our application should now be able to keep track of our logged in status. You can click each button and use the Back button to see it in action:

Log out

Redirect Page

Let's create one more component that we'll redirect users to if they try to access our /dashboard route without first logging in. We'll make this component look a little more interesting than the rest of our black and white app by copying this cool 403 page from codepen by user @anjanas_dh

In the components directory, make a file called Unauthorized.js and add the following markup.

protected-routes/src/Unauthorized.js
import React from 'react';
import { Link } from 'react-router-dom';
import '../Unauthorized.scss';

const Unauthorized = () => {
  return (
    <div className='container'>
      <div class="gandalf">
        <div class="fireball"></div>
        <div class="skirt"></div>
        <div class="sleeves"></div>
        <div class="shoulders">
          <div class="hand left"></div>
          <div class="hand right"></div>
        </div>
        <div class="head">
          <div class="hair"></div>
          <div class="beard"></div>
        </div>
      </div>
      <div class="message">
        <h1>403 - You Shall Not Pass</h1>
        <p>Uh oh, Gandalf is blocking the way!<br />Maybe you have a typo in the url? Or you meant to go to a different location? Like...Hobbiton?</p>
      </div>
      <p><Link to='/'>Back to Home</Link></p>
    </div>
  )
}

export default Unauthorized;

Create a new SCSS file called Unauthorized.scss in the src directory and paste in these styles. I included the link to a pastebin rather than the code itself since the file is 270 lines long.

Since this is a Sass file, it won't work out of the box, but don't fret! We only need to install the node-sass module in order to get us on our wizard way 🧙‍♂️.

yarn add node-sass

Open up App.js and import the Unauthorized component and add it to our list of Route components.

import Unauthorized from './comoponents/Unauthorized';
/* omitting some of the other LOC to save space */
  return (
    <div className="App">
      <Router>
        <Route exact path='/' handleLogin={handleLogin} render={
          props => <Landing {...props} user={user.toString()}
            handleLogin={handleLogin} />} />
        <ProtectedRoute exact path='/dashboard' handleLogout={handleLogout} component={Dashboard} />
        <Route exact path='/unauthorized' component={Unauthorized} />
      </Router>
    </div>
  );
/* omitting some of the other LOC to save space */

If all went according to plan, you should see the following page when you navigate to '/unauthorized'

Unauthorized page

Protecting the Route

Ok, now we're in the home stretch! Let's add in the logic to ensure we're logged in before we can view the Dashboard component. First, let's pass in our user state as a prop to our ProtectedRoute in App.js.

protected-routes/src/App.js
<ProtectedRoute exact path='/dashboard' user={user} handleLogout={handleLogout} component={Dashboard} />

Jump back into ProtectedRoute.js and let's add a conditional that checks if our user status is set to true, in which case we'll render the Component, otherwise, redirect to our /unauthorized route. Therefore we'll need to import the <Redirect> component from react-router as well. Here's what the final ProtectedRoute component should look like:

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

const ProtectedRoute = ({ component: Component, user, ...rest }) => {
  return (
    <Route {...rest} render={
      props => {
        if (user) {
          return <Component {...rest} {...props} />
        } else {
          return <Redirect to={
            {
              pathname: '/unauthorized',
              state: {
                from: props.location
              }
            }
          } />
        }
      }
    } />
  )
}

export default ProtectedRoute;

Note, we're now specifying user as one of the props we're passing into our ProtectedRoute component since we're referencing it a little later on in our if statement that checks if we've "logged in" or not.

  • If user evaluates to true, then we'll render our component as normal
  • However, if it's set to false, we'll use the <Redirect> component from react-router to redirect the user to '/unauthorized'.

Alright, now the moment of truth. Let's try accessing the "View Dashboard" link without first "logging in". We should be greeted by Gandalf the Grey.

Gandalf you shall not pass

Now let's click the log in button to simulate authentication. Our status changes to true and when we click on the "View Dashboard" link, our app should now render the Dashboard component. If we click "Log Out", we'll be immediately booted to the Unauthorized page.

Demo

Summary

We got a glimpse of how we can protect private pages with react-router and our <ProtectedRoute> higher-order component. We utilized the useState hook to give our function components access to state, which we passed down as props to child components. As mentioned at the onset, we'll take a look in a future post at how we can use the Context API to avoid having to prop drill.

If you made it to the end, thanks for reading. I appreciate any comments or suggestions, so feel free to leave them below. As always, happy hacking!

Posted on Jan 21 by:

mychal profile

mychal

@mychal

I build things with code, mainly Node, React, and GraphQL. Outdoor enthusiast and hobbyist photographer.

Discussion

markdown guide
 

What a wonderful guide!
There were a couple things that caught me while following along:

  1. You misspelled "components" in "import Unauthorized from './comoponents/Unauthorized';"
  2. Also it seems like you don't need the prop "handleLogin" in the Route component for "<Route exact path='/' handleLogin={handleLogin} render={..." unless it's there for some reason I'm unaware of.

Thank you so much for this article though! It was very helpful for me!

 

great article
i have tried to do the same thing in my application but i have a problem
the data that i passed to the ProtectedRoutes are undefined i don't know why

at first the date are present but once i click on the login button which must change the Auth state to true after an API call.
but once i receive the data they are lost in the ProtectedRoute and are set to undefined

 

Thanks for sharing your knowledge. I don't understand why implementing this idea in my other project took me a long time. You made it so simple to protect private routes in ReactJS. Thanks again.

 
 

No problem, thanks for reading!

 

Not work if i refresh '/dashboard'

 

@juanortizoviedo - I had the exact same problem so once the web browser is refreshed whilst the URL is /dashboard, the page is automatically redirected to /unauthorized which makes sense as this is specified in ProtectedRoute.

I tried refreshing the browser whilst on /unauthorized page and it worked as expected (reloaded /unauthorized as normal).

I've since created a fresh create-react-app project with firebase and authenticating a user with either email or google provider and this exact same refresh issue is happening. It's almost as if the user is authenticated quickly enough on page load so the browser redirects the specified page or back to the root (/) URL.

So if a user (authenticated or not authenticated) refreshes the browser on a non-protected page, everything is fine but they are redirected if already on a protected page.

Have you managed to figure out how to overcome this issue? Any help would be greatly appreciated @mychal

 
 

Very useful, thank you very much!

 
 

This is really good guide. I quite enjoyed it. Very easy to follow and concise too. Well done @mychal

 
 

Thank you for the detailed breakdown of Protected Routing Mychal!

 

Thanks! I was struggling with other tutorials to understand the HOC that abstract the logic of protecting the components but now I understand thanks to you.