Handling User Sessions with React Context

April 28, 2019

One of the most fun aspects of starting a new project is getting to work with the latest technologies. This time around we chose to keep React but were finally able to explore the new hooks and context implementations. Having set up our redux stores pretty traditionally, we wanted to explore other options for user authentication and session management. This would mean being able to leave our redux stores for just regular API data. We ended up choosing to keep the state stored in just cookies, and then using the latest react context APIs to be able to read and update the data throughout the app.

It is not a new idea to keep session data in cookies. They persist through user session, can have a set expiration if necessary, specific to each domain, are widely supported in any browser and can be transferred via headers with your backend. Furthermore, then can be accessed from anywhere in the javascript like a global so you will not be stuck using React Context if you’re outside a component. This is particularly useful for actual API calls that need a token or some other authorization.

With that, let’s dive in to an example of how login/logout could be handled with React Context and react-router. See the sandbox example here.

Dependencies

This walkthrough assumes you have a setup React project that is at least @16.8 (when hooks were introduced). Thankfully, the latest create-react-app has stable hook support.

We will also need the following packages from npm:

  • history@4.9.0
  • js-cookie@2.2.0
  • react-router@5.0.0
  • react-router-dom@5.0.0

Setting up the routes

You can start from almost anywhere you would like, but I think setting up the routes and the project structure is the best place. Doing so prescribes the route handlers that we will require. Furthermore, react context provider will be wrap all of the routes so that any handler or child can consume the context variables.

We start with just the simple structure of the application, which should be pretty familiar to most react codebases:

import React from "react";
import { Router, Switch, Route } from "react-router";
import { createBrowserHistory } from "history";

const Routes = () => {
  return (
    <Router history={history}>
      <div className="navbar">
        <h6 style={{ display: "inline" }}>Nav Bar</h6>
      </div>
      <Switch>
        <Route path="/login" component={LoginHandler} />
        <Route path="/logout" component={LogoutHandler} />
        <Route path="*" component={ProtectedHandler} />
      </Switch>
    </Router>
  );
};

const App = () => (
  <div className="App">
    <Routes />
  </div>
);

Next, we just set up some simple route handlers to get the app compiling.

const LoginHandler = () => {
  return (
    <div style={{ marginTop: "1rem" }}>
      <form onSubmit={handleSubmit}>
        <input
          type="email"
          placeholder="Enter email address"
        />
        <input type="submit" value="Login" />
      </form>
    </div>
  );
};

const LogoutHandler = () => {
  return <div>Logging out!</div>;
};

const ProtectedHandler = () => {
  return (
    <div>
      <Link to="/logout">Logout here</Link>
    </div>
  );
};

Each handler is extremely basic right now, and adding the functionality will be our next task. As of right now, however, there are just three route handlers:

  • LoginHandler => /login - This component allows us to login and set the necessary cookies/context
  • LogoutHandler => /logout - This component has almost no render() method, but instead on mount will remove our cookies and redirect to the /login page
  • ProtectedHandler => /* - This is a catch-all for routes that are session/password protected. In our case, we will use this component to access session information from the context and either use it or just redirect to /login if not authorized.

Managing cookies

Cookies can be frustrating to work with so I recommend using a library and abstracting any usage into a utils file or helper functions. In this example, I use js-cookie but there are numerous ways to handle cookies. We are going to write two functions: one for setting the cookie and the other for getting a cookie. (In the codesandbox, this is under sessions.ts).

import * as Cookies from "js-cookie";

export const setSessionCookie = (session: any): void => {
  Cookies.remove("session");
  Cookies.set("session", session, { expires: 14 });
};

export const getSessionCookie: any = () => {
  const sessionCookie = Cookies.get("session");

  if (sessionCookie === undefined) {
    return {};
  } else {
    return JSON.parse(sessionCookie);
  }
};

The typescript is not necessary, but it makes simplifies dealing with the undefined more straightforward (and is useful for managing the null states when a cookie is not set).

Adding cookies to the login/logout handlers

The last thing we have to do before adding context is the management of cookies themselves. This means two things: setting the cookie on successful login and removing the cookie on any logout.

Adding cookies to the LoginHandler

Let us first update the the form to take and submit input. We will use the useState hook and write a function in the handler to handle the event on submit. Note, that there are many more ways of doing this and this is just the most straightforward for this point of this walkthrough.

const LoginHandler = ({ history }) => {
  const [email, setEmail] = useState("");
  const [loading, setLoading] = useState(false);
  const handleSubmit = async e => {
    e.preventDefault();
    setLoading(true);
    // NOTE request to api login here instead of this fake promise
    await new Promise(r => setTimeout(r(), 1000));
    setSessionCookie({ email });
    history.push("/");
    setLoading(false);
  };

  if (loading) {
    return <h4>Logging in...</h4>;
  }

  return (
    <div style={{ marginTop: "1rem" }}>
      <form onSubmit={handleSubmit}>
        <input
          type="email"
          placeholder="Enter email address"
          value={email}
          onChange={e => setEmail(e.target.value)}
        />
        <input type="submit" value="Login" />
      </form>
    </div>
  );
};

For a more realistic login, you would need to add a password, etc. Here, we just take any email as valid, and set it as the session cookie. If you are unfamiliar, with the new useState hook, see here for more info. This new hook makes adding form values and events listener much simpler to work with!

As far as cookie management, however, we are just setting an object with { email } as the value. In our main app, we ended up choosing to store a whole user object, however, it’s all that you require in your app to manage the current session.

Lastly, we just use history, provided from react-router, to push to a protected route.

Adding cookies to the LogoutHandler

The LogoutHandler is even simpler: we just remove the cookie and redirect to the login page. For this, we use the useEffect hook to handle componentDidMount functionality. The nice part about logging out this way is that you just have to Link to /logout anytime you want to have logout functionality.

See below:

const LogoutHandler = ({ history }) => {
  useEffect(
    () => {
      Cookies.remove("session");
      history.push("/login");
    },
    [history]
  );

  return <div>Logging out!</div>;
};

Implementing as context

Now, in order to take advantage of having the session (or not) data and using it in a react component, we implement the session object as React Context.

The first thing that has to be done is establish the context as a variable that can be accessed globally. I added this in session.ts, but it can be placed anywhere that is easily accessible (constants maybe?).

export const SessionContext = React.createContext(getSessionCookie());

The parameter of createContext takes in the default value. We just assign whatever is in the session cookie, if any. getSessionCookie() return an empty object if there is session stored.

Now, in order to use the context through out the app, we have to provide it. Since we need session data throughout all of the routes, it makes sense to wrap the routes in the Provider component. Import the SessionContext and wrap Router:

  return (
    <SessionContext.Provider value={}>
      <Router history={history}>
        {/* routes */}
      </Router>
    </SessionContext.Provider>
  );

Provider, however, also takes a prop called value which will overwrite the default value originally provided to the context. Using the value as state variable, we can tie in our cookies.

const Routes = () => {
  const [session, setSession] = useState(getSessionCookie());

  return (
    <SessionContext.Provider value={session}>
      <Router history={history}>
        {/* routes */}
      </Router>
    </SessionContext.Provider>
  );
};

And, then finally, if a use does login or logout, we will need to update the session state variable. To do this, we use a lifecycle method in the form of a useEffect hook. The final Routes component is shown below:

const Routes = () => {
  const [session, setSession] = useState(getSessionCookie());
  useEffect(
    () => {
      setSession(getSessionCookie());
    },
    [session]
  );

  return (
    <SessionContext.Provider value={session}>
      <Router history={history}>
        {/* routes */}
      </Router>
    </SessionContext.Provider>
  );
};

With the useEffect hook, we can be optimized by subscribing to changes on session.

Now our session is stored throughout the app!

Accessing the context in the components

There are many things we can do the session available but two necessary ones are exemplified in the ProtectedHandler component. Either way, we first have to access the context using the useContext hook (you could also use the SessionContext.Consumer as a wrapper like we did with Provider). In the function, import SessionContext and call useContext:

const session = useContext(SessionContext);

Redirecting for unauthenticated apps

After accessing the session variable, you can simply push to /login if it is undefined. (If you are not using Typescript, then this undefined check does not work).

if (session.email === undefined) {
   history.push("/login");
}

Please note that it can be this simple, but it gets complicated very fast in a large app that it might make sense to abstract checking on the session differently. Maybe a custom hook make sense or nesting react-router routes might work for you.

Using session data in a component

Another simple example of using this session data is also shown in the render() method.

  return (
    <div>
      <h6>Protected data for {session.email}</h6>
      {...}
    </div>
  );

This example is very straightforward, but it is useful in situations where you need to show user information in a nav bar or settings pages, etc.


Thank you for reading! See the codesandbox here for a working example. If you have any questions or want to discuss this solution feel free to find me on Twitter or email me at jmfurlott@gmail.com.


Written by Joseph Furlott who lives and works in Brooklyn, New York. I am a software engineer that specializes in designing and building web applications using React. I work at Datadog as a software engineer.

© 2022
jmfurlott@gmail.com