Protected Routes with React Function Components

mychal

mychal

Posted on January 21, 2020

Protected Routes with React Function Components

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
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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);
  }
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 }) => {
Enter fullscreen mode Exit fullscreen mode
  • 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} />
Enter fullscreen mode Exit fullscreen mode
  • 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>
  );
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 */
Enter fullscreen mode Exit fullscreen mode

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} />
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
mychal
mychal

Posted on January 21, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related