We will be using hooks and context. We will only use the basic concept, you don't need to go too far into this subject for this use case.
What do we need to do?
- Create a page that will be accessible only after sign in (we need to create 2 pages: the SignIn page where the user logs in and the Panel page where the user goes after SignIn. The user can access the Panel page only after SignIn. If he is trying to access Panel directly, we need to redirect him to SignIn);
- If the user is already logged in and refreshes the page, he should stay on the Panel page and not be redirected to the SignIn page;
How will we do it?
- We will create a component called PrivateRoute which will be accessible only after passing SignIn page;
- We will save the user token in localStorage so when he quits or refreshes a page, he can access the Panel directly.
Now that we understood what we will do, we can start coding.
Creating our components: Panel and SignIn
First of all, in our src folder, we will create a new folder which is called screens. Here we will create Panel.js and SignIn.js. I will use bootstrap to style my components faster. If you want to do the same and you don't know how to install bootstrap, please look here.
In src/screens/Panel.js:
import React from "react";
import { Button } from "react-bootstrap";
const Panel = () => {
const onLogOut = () => {
console.log('LogOut pressed.'); // we will change it later
}
return (
<div
style={{ height: "100vh" }}
className="d-flex justify-content-center align-items-center"
>
<div style={{ width: 300 }}>
<h1 className="text-center"> Hello, user </h1>
<Button
variant="primary"
type="button"
className="w-100 mt-3 border-radius"
onClick={onLogOut}
>
Log out
</Button>
</div>
</div>
);
};
export default Panel;
In src/screens/SignIn.js:
import React, { useState} from 'react';
import { Form, Button } from 'react-bootstrap';
const SignIn = () => {
const [email, setEmail] = useState();
const [password, setPassword] = useState();
const onFormSubmit = e => {
e.preventDefault();
console.log(email);
console.log(password);
// we will change it later;
};
return (
<div
style={{ height: "100vh" }}
className="d-flex justify-content-center align-items-center"
>
<div style={{ width: 300 }}>
<h1 className="text-center">Sign in</h1>
<Form onSubmit={onFormSubmit}>
<Form.Group>
<Form.Label>Email address</Form.Label>
<Form.Control
type="email"
placeholder="Enter email"
onChange={e => {
setEmail(e.target.value);
}}
/>
</Form.Group>
<Form.Group>
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
placeholder="Password"
onChange={e => {
setPassword(e.target.value);
}}
/>
</Form.Group>
<Button
variant="primary"
type="submit"
className="w-100 mt-3"
>
Sign in
</Button>
</Form>
</div>
</div>
);
};
export default SignIn;
Now we need to create our router. We will do it in App.js. For navigation in our app, we will be using react-router-dom. We need to install it with yarn or npm:
yarn add react-router-dom
Now in src/App.js we will create routes for our app.
import React from 'react';
import { Switch, BrowserRouter, Route } from 'react-router-dom';
import SignIn from './screens/SignIn';
import Panel from './screens/Panel';
function App() {
return (
<BrowserRouter>
<Switch>
<Route path="/sign-in" component={SignIn} />
<Route path="/" component={Panel} />
</Switch>
</BrowserRouter>
);
}
export default App;
Saving the user token in the context
Now we need to create a context to be able to access the user token in multiple components. Even if in this example we have only 2 components but in real-life applications, we will have much more and a lot of them will need user's information.
We will create a folder called contexts in the src folder and will create AuthContext.js.
In src/contexts/AuthContext.js:
import React, { createContext, useState } from 'react';
export const authContext = createContext({});
const AuthProvider = ({ children }) => {
const [auth, setAuth] = useState({ loading: true, data: null });
// we will use loading later
const setAuthData = (data) => {
setAuth({data: data});
};
// a function that will help us to add the user data in the auth;
return (
<authContext.Provider value={{ auth, setAuthData }}>
{children}
</authContext.Provider>
);
};
export default AuthProvider;
To be able to use our context in the whole application we need to wrap our App component in AuthProvider. To do this we go in src/index.js:
...
import AuthProvider from './contexts/AuthContext';
ReactDOM.render(
(
<AuthProvider>
<App />
</AuthProvider>
),
document.getElementById('root'),
);
...
Now we need to pass the user credentials to the context from the SignIn component. Ideally, you would only send the token to the context, but in this example, we will send the user email, as we do not have a backend to provide us one.
In src/screens/SignIn.js:
...
import React, { useState, useContext } from 'react';
import { authContext } from '../contexts/AuthContext';
const SignIn = ({history}) => {
...
const { setAuthData } = useContext(authContext);
const onFormSubmit = e => {
e.preventDefault();
setAuthData(email); // typically here we send a request to our API and in response, we receive the user token.
//As this article is about the front-end part of authentication, we will save in the context the user's email.
history.replace('/'); //after saving email the user will be sent to Panel;
};
...
};
export default SignIn;
Also, when the user clicks Log out button in the Panel, we need to clear our context. We will add the user email instead of "Hello, user". In src/screens/Panel.js:
import React, {useContext} from "react";
import { Button } from "react-bootstrap";
import { authContext } from "../contexts/AuthContext";
const Panel = () => {
const { setAuthData, auth } = useContext(authContext);
const onLogOut = () => {
setAuthData(null);
} //clearing the context
return (
<div
style={{ height: "100vh" }}
className="d-flex justify-content-center align-items-center"
>
<div style={{ width: 300 }}>
<h1 className="text-center"> {`Hello, ${auth.data}`} </h1>
<Button
variant="primary"
type="button"
className="w-100 mt-3"
onClick={onLogOut}
>
Log out
</Button>
</div>
</div>
);
};
export default Panel;
Creating a PrivateRoute
Now we need to make the Panel accessible only after signing in. To do this we need to create a new component called PrivateRoute. We are creating src/components/PrivateRote.js:
import React, { useContext } from 'react';
import { Route, Redirect } from 'react-router-dom';
import { authContext } from '../contexts/AuthContext';
const PrivateRoute = ({ component: Component, ...rest }) => {
const { auth } = useContext(authContext);
return (
<Route
{...rest}
render={(routeProps) => (
auth.data ? <Component {...routeProps} /> : <Redirect to="/sign-in" />
)}
/>
);
/* we are spreading routeProps to be able to access this routeProps in the component. */
};
export default PrivateRoute;
If a user is not logged in we will redirect him to the SignIn component.
Now we need to use our PrivateRoute in src/App.js:
...
import PrivateRoute from './components/PrivateRoute';
function App() {
return (
<BrowserRouter>
<Switch>
<Route path="/sign-in" component={SignIn} />
<PrivateRoute path="/" component={Panel} />
</Switch>
</BrowserRouter>
);
}
export default App;
Managing localStorage
Now everything works, but if we refresh our Panel page we will return to SignIn. We want the browser to remember the user. For this reason, we will be using localStorage. LocalStorage is a place that stores data in the browser. The problem with localStorage is that it slows down the application. We need to use it wisely and put in useEffect function to ensure the code only executes once. We will do all the manipulation in src/contexts/AuthContext.js:
import React, { createContext, useState, useEffect } from 'react';
export const authContext = createContext({});
const AuthProvider = ({ children }) => {
const [auth, setAuth] = useState({ loading: true, data: null });
const setAuthData = (data) => {
setAuth({data: data});
};
useEffect(() => {
setAuth({ loading: false, data: JSON.parse(window.localStorage.getItem('authData'))});
}, []);
//2. if object with key 'authData' exists in localStorage, we are putting its value in auth.data and we set loading to false.
//This function will be executed every time component is mounted (every time the user refresh the page);
useEffect(() => {
window.localStorage.setItem('authData', JSON.stringify(auth.data));
}, [auth.data]);
// 1. when **auth.data** changes we are setting **auth.data** in localStorage with the key 'authData'.
return (
<authContext.Provider value={{ auth, setAuthData }}>
{children}
</authContext.Provider>
);
};
export default AuthProvider;
Now in src/components/PrivateRoute.js:
const PrivateRoute = ({ component: Component, ...rest }) => {
const { auth } = useContext(authContext);
const { loading } = auth;
if (loading) {
return (
<Route
{...rest}
render={() => {
return <p>Loading...</p>;
}}
/>
);
}
// if loading is set to true (when our function useEffect(() => {}, []) is not executed), we are rendering a loading component;
return (
<Route
{...rest}
render={routeProps => {
return auth.data ? (
<Component {...routeProps} />
) : (
<Redirect to="/sign-in" />
);
}}
/>
);
};
export default PrivateRoute;
That's it. Now if the user is logged in and he refreshes a page he stays on a Panel and is not redirected to SignIn. However, if the user logged out, he can access Panel only by passing by SigIn.
Why did we use the loading object in our context?
The setAuth function which we used in the context is asynchronous it means it takes some time to really update the state. If we didn't have the loading object, for some milliseconds auth.data would be null. For this reason, we are setting loading to false in our context and return the needed route in the PrivateRoute component.
Top comments (14)
I'm not sure storing the credentials locally is the way to go. Also the redirection is easily bypassable by setting an arbitrary auth.data.
I agree. I would suggest using Auth0. They have a good example on the Auth0 website.
We would not save the user credentials in the browser local storage, but rather just the token received after authentication. As I said, the tutorial do not cover the backend portion of it but rather saving the token and blocking access to "authenticated" routes. You could use it to implement Auth0, which could actually be a nice continuation of my tutorial! :)
EDIT: I agree my article does not emphasize this enough. I updated the article to add a disclaimer to save the token and not the actual credentials. Thank you!
Hi, firstly it's a great article. Implementing the back end token code was beyond the scope in my opinion. What it demonstrates is exactly what you said in your reply. And the private route implementation is good as well.
Awesome! I'm currently learning React, this will help 🙂
In AuthContext.js this bit is causing an error for me:
const setAuthData = (data) => {
setAuth({data: data});
};
It is saying
Argument types do not match parameters
. So I addedloading: false
to the object passed in tosetAuth
and the error went away.Great stuff! 👏🏾
However may I present something to you?
Lets say I was your boss and I said "...right, just what we need. What do we need to do, to get this into production?..."
What would your response be?
Thanks for this tutorial! It really helped me and my team with a task 😁
Thank you miss, your tutorial was very helpful to me.
Nice one from you dear, the article was well-detailed and really helpful...
The best react authentication i've found on internet. You're awesome !!!
Great article. It would be great to see this extended with JWT and a database. Or using Auth0.
Great.. Help!!! Thanks for sharing!!