Authentication with React.js
Oksana Ivanchenko
Posted on January 16, 2020
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.
Posted on January 16, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.