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.
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
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.
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:
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'
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.
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.
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!
Top comments (24)
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 inProtectedRoute
.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
Have a read over the answer here: stackoverflow.com/questions/279283...
Client side routing is hard! Basically what you're seeing is the server try to find something at /route, when your SPA loads from /index.html - /route doesn't 'really' exist - until your index.html is read and your JS kicks in. It really blows your mind until you get it. Using react router you can evade this issue by using hashrouting
@franciscobenedict I had this exact same issue. Firebase will persist an authenticated users details in local storage, however to retrieve them initially is asynchronous which as you've experienced doesn't work when refreshing the page and you get taken to a login screen, etc.
As we will be wanting to know about this user state across multiple components its advised to wrap all this logic into a context provider. To start solving this we can utilise
onAuthStateChanged
from Firebase which is the recommend way of retrieving a user as it allows Firebase to initialise, it will provide either the user object ornull
in the callback. What we also need is a loading state, which is true on initial launch and when the user has been loaded or if there isn't one we can stop loading. To get this behaviour we can wraponAuthStateChanged
in a promise which resolves the user and call it on initial launch usinguseEffect
. We can then wait for this promise to settle before we manage our final loading state. Finally we can decide what to do once we've finished loading and we either have or don't have a user.Here's my code:
Helper function
AuthContext.js
ProtectedRoute.js
The issue seems to be as simple as the demo application doesn't have a persisting state of the user's auth. Since it's just artificially creating auth for a user and storing it in memory. The state of the user being logged in can't survive a page refresh. So naturally when you login and going to dashboard via the on page links it works but when you do a page refresh the user's current logged in state is lost from memory and defaults back to its original state of false. Look at the React Developer Tools and monitor the components state regarding the user's auth. A solution to this in a real application would be to have some state management system like context or Redux to persist the state so the accurate page shows when expected.
I came across this issue when working with protected routes. The reason as you've already figured out, is due to the user state initially being sent to our components (defaults as false), so when it hits our protected route, it redirects the user to 401 unauthorised. What I did to solve this issue was check the user state before returning the routes. To "load" the app before it actually hit the router. I've attached some sample code, hopefully helps someone stuck with this.
gist.github.com/FaizanH/88ede257df...
There must be a typo
What a wonderful guide!
There were a couple things that caught me while following along:
Thank you so much for this article though! It was very helpful for me!
Unfortunately when I try this approach and hit the protected route, I get a show-stopping error: 'Invariant failed: You should not use <Route> outside a <Router>'. Seems like it doesn't like me composing something around the Route component, but I don't know why you and I get different results for that.
Thank you :-)
No problem, thanks for reading!
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.
Anyway to implement this with page refresh?
Thank you , it was very helpful
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