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/contextLogoutHandler
=>/logout
- This component has almost norender()
method, but instead on mount will remove our cookies and redirect to the/login
pageProtectedHandler
=>/*
- 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.