Authentication with React.js

ksushiva

Oksana Ivanchenko

Posted on January 16, 2020

Authentication with React.js

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?

  1. 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);
  2. 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?

  1. We will create a component called PrivateRoute which will be accessible only after passing SignIn page;
  2. 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;
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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'),
);

...
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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

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

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;

Enter fullscreen mode Exit fullscreen mode

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

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

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.

💖 💪 🙅 🚩
ksushiva
Oksana Ivanchenko

Posted on January 16, 2020

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

Sign up to receive the latest update from our blog.

Related

Authentication with React.js
react Authentication with React.js

January 16, 2020