DEV Community

Cover image for Authentication with React.js
Oksana Ivanchenko
Oksana Ivanchenko

Posted on • Edited on

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.

Top comments (14)

Collapse
 
amine1996 profile image
amine1996 • Edited

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.

Collapse
 
bernardbaker profile image
Bernard Baker

I agree. I would suggest using Auth0. They have a good example on the Auth0 website.

Collapse
 
ksushiva profile image
Oksana Ivanchenko • Edited

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!

Thread Thread
 
bernardbaker profile image
Bernard Baker

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.

Collapse
 
vaibhavkhulbe profile image
Vaibhav Khulbe

Awesome! I'm currently learning React, this will help 🙂

Collapse
 
dmikester1 profile image
Mike Dodge

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 added loading: false to the object passed in to setAuth and the error went away.

Collapse
 
okbrown profile image
Orlando Brown • Edited

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?

Collapse
 
josuerodriguez98 profile image
Josué Rodríguez (He/Him)

Thanks for this tutorial! It really helped me and my team with a task 😁

Collapse
 
maydara86 profile image
Maydara86

Thank you miss, your tutorial was very helpful to me.

Collapse
 
devorji profile image
Chigbogu Orji

Nice one from you dear, the article was well-detailed and really helpful...

Collapse
 
biciatoleandro profile image
Leandro Biciato

The best react authentication i've found on internet. You're awesome !!!

Collapse
 
bernardbaker profile image
Bernard Baker

Great article. It would be great to see this extended with JWT and a database. Or using Auth0.

Collapse
 
fahadashiq12 profile image
Fahad Ashiq

Great.. Help!!! Thanks for sharing!!