As your application complexity increases, you may start thinking about implementing a session timeout in instances when there is no activity for a period of time. In this post, I’ll walk through using Material-UI with React to create a timeout session. Whether you want a session timeout to increase your web app securities or to avoid unnecessary automatic API calls, it’s good to have some sort of idle check and log out built into your application.
The Basics
Let’s say you want to implement a timeout in your React-based app. How would you get started? I would suggest using Material-UI with React, but before I show you, we need to understand how a web application can detect user activities and tell whether or not the user is idle.
The most basic way to detect whether a user is idle or not is on your app using an event listener like onmousemove
or onkeypress
. Here’s the basic approach:
const userActivities = () => { let timeout; window.onload = resetTimeout; document.onmousemove = resetTimeout; document.onkeypress = resetTimeout; function resetTimer() { clearTimeout(time); time = setTimeout(() => console.log("logout"), 60000) } }
The thought is that every time a user moves their mouse or presses a key, the app will reset the timer. However, if neither of these events occur within the specified time, the app will log out and return the user to the login page.
The Implementation of Material-UI into a React App
Ok, now that we’ve got the basics down. How can we implement this feature in an elegant way?
To do that, we need the following:
git clone https://github.com/peterhle/react-session-timeout.git
cd react-session-timeout
git checkout fresh-start
npm install react-idle-timer --save or yarn add react-idle-timer
The initial project link contained @material-ui
as a package dependency. This is what we will be using as our app UI components.
Once installed, the first thing we want to do is to add a new component at the root level.SessionTimeout.jsx
import React, { useContext, useRef, useState } from "react"; import IdleTimer from "react-idle-timer"; export default function SessionTimeout() { const idleTimer = useRef(null); const {handleLogoutUser} = useContext(AppStateContext); const onActive = () => { console.log("active"); // timer reset automatically. }; const onIdle = () => { console.log("idle") handleLogoutUser(); }; return ( <> <IdleTimer ref={idleTimer} onActive={onActive} onIdle={onIdle} debounce={250} timeout={5000} /> </> ); }
As you can see above, we imported the IdleTimer from the package react-idle-timer
and loaded that in as our session idle detector.
There are three functions on this package that you’ll want to learn.
onIdle
: A function that will be triggered when a user has been idle for a specified time in the “timeout” proponActive
: Triggered when a user becomes active again after being idledonAction
(not used above): Triggered every time there’s a mouse event or keyboard event (i.e. a mouse move, mouse click, or keypress)
With that, we need a way to incorporate this new component into our app. For our application, Dashboard.jsx
would be the component that would be landed after we logged in, so this will be where we add our new component.
import SessionTimeout from "../SessionTimeout"; function Dashboard() { return ( <> <SessionTimeout /> ... </> ); }
I set the timeout to 5 seconds for demonstration purposes. Normally, you’ll want to set this to be higher, more like 15 minutes.
With that said, we can now run yarn start
to load the application up.
Simple enough! However, more likely than not, you don’t want the app to log you out without some kind of notification.
For that reason, we are going to implement a countdown dialog that begins when a certain time is hit. For example, when there are 60 seconds left on the timer, we’ll show the countdown dialog. This will allow the user to do one of two things: click continue, resulting in the timer resetting and the dialog being hidden OR choose to log out or let the time run out, which would send the user back to the log in screen.
Here’s how we would go about accomplishing that. First, we’ll need to add a new component for the dialog.SessionTimeoutDialog.jsx
import React from "react"; import * as PropTypes from "prop-types"; import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography, makeStyles, Slide } from "@material-ui/core"; import clsx from "clsx"; import red from "@material-ui/core/colors/red"; const useStyles = makeStyles(() => ({ dialog: { borderRadius: 0 }, button: { borderRadius: 0, textTransform: "none", padding: 5 }, logout: { color: "#fff", backgroundColor: red[500], "&:hover": { backgroundColor: red[700] } }, countdown: { color: red[700] } })); const Transition = React.forwardRef(function Transition(props,ref) { return <Slide direction="up" ref={ref} {...props} />; }); export default function SessionTimeoutDialog({ open, countdown, onLogout, onContinue }) { const classes = useStyles(); return ( <Dialog open={open} aria-labelledby="session-timeout-dialog" aria-describedby="session-timeout-dialog" classes={{ paper: classes.dialog }} TransitionComponent={Transition} > <DialogTitle id="session-timeout-dialog-title"> Session Timeout </DialogTitle> <DialogContent> <Typography variant="body2"> The current session is about to expire in{" "} <span className={classes.countdown}>{countdown}</span> seconds. </Typography> <Typography variant="body2">{`Would you like to continue the session?`}</Typography> </DialogContent> <DialogActions> <Button onClick={onLogout} variant="contained" className={clsx(classes.logout, classes.button)} > Logout </Button> <Button onClick={onContinue} color="primary" variant="contained" className={classes.button} > Continue Session </Button> </DialogActions> </Dialog> ); } SessionTimeoutDialog.propTypes = { /** * indicator whether the dialog is open/close */ open: PropTypes.bool.isRequired, /** * the countdown timer. */ countdown: PropTypes.number.isRequired, /** * callback function to handle closing action */ onLogout: PropTypes.func.isRequired, /** * callback function to handle confirm action. */ onContinue: PropTypes.func.isRequired };
Then, we’ll need to add that to our SessionTimeOut.jsx
component.
<IdleTimer ... /> <SessionTimeoutDialog countdown={timeoutCountdown} onContinue={handleContinue} onLogout={handleLogout} open={timeoutModalOpen} />
Notice that we have few props that we’ll need to add to the same SessionTimeOut.jsx
component.
The next steps are to add a couple of states to the component and import our unauthenticated
indicator from the app context.
const { isAuthenticated, handleLogoutUser } = useContext(AppStateContext); const [timeoutModalOpen, setTimeoutModalOpen] = useState(false); const [timeoutCountdown, setTimeoutCountdown] = useState(0);
We also need to modify our IdleTimer’s onIdle
and onActive
function to handle the countdown and reset.
const onActive = () => { if (!timeoutModalOpen) { clearSessionInterval(); clearSessionTimeout(); } }; const onIdle = () => { const delay = 1000 * 5 // 5 seconds for demonstration purposes; if (isAuthenticated && !timeoutModalOpen) { timeout = setTimeout(() => { let countDown = 60; setTimeoutModalOpen(true); setTimeoutCountdown(countDown); countdownInterval = setInterval(() => { if (countDown > 0) { setTimeoutCountdown(--countDown); } else { handleLogout(); } }, 1000); }, delay); } };
The above code shows that when a user is active, we’ll clear any timeout/interval set while they were last idled. Within the onIdle
function, we have a setTimeout
for a period of time (in this case 5 seconds). Once time runs out, the countdown timer will open and begin counting down for 60 seconds. At this point, a user can decide to continue, log out, or let the time count to 0, which results in the page automatically logging the user out.
Lastly, for this dialog, we’ll need to add a few functions to handle whether a user wants to continue or log out.
const clearSessionTimeout = () => { clearTimeout(timeout); }; const clearSessionInterval = () => { clearInterval(countdownInterval); }; const handleLogout = () => { setTimeoutModalOpen(false); clearSessionInterval(); clearSessionTimeout(); handleLogoutUser(); }; const handleContinue = () => { setTimeoutModalOpen(false); clearSessionInterval(); clearSessionTimeout(); };
Since the page can log out automatically, we’ll need some kind of message to display after it logs out to let the user know why.
To do so, we will add a new prop to our existing AppStateContext.jsx
component.
const initialState = { isAuthenticated: false, isTimedOut: false };
Edit the reducer for LOGOUT
.
case LOGOUT: return { ...initialState, isTimedOut: action.isTimedOut };
Edit the handleLogoutUser
function with a isTimedout
indicator.
function handleLogoutUser(isTimedOut = false) { dispatch({ type: LOGOUT, isTimedOut }); navigate("/"); }
Modify our handleLogOut
function on SessionTimeout
component to pass in the indicator.
const handleLogout = (isTimedOut = false) => { ... handleLogoutUser(isTimedOut); } const onIdle = () => { ... handleLogout(true); }; <SessionTimeoutDialog ... onLogout={() => handleLogout(false)} >
Finally, we’ll need a message displayed explaining that the log out was caused by a timed out. Modify the LogIn
component, and add a message.
const {handleAuthenticateUser, isTimedOut} = useContext(AppStateContext); return ( <Container component="main" maxWidth="sm"> ... {isTimedOut && ( <> <Typography className={classes.title}>Session Timed Out</Typography> <Paper elevation={0} className={classes.timeoutPaper}> <ErrorOutline className={classes.icon} /> <div className={classes.message}> <Typography> <span className={classes.placeholderText}> Your current session has timed out. </span> <div className={classes.placeholderText}> Please log in again to continue. </div> </Typography> </div> </Paper> </> )} </Container> )
And that’s it! Let’s run our application and allow the countdown to run out.
Conclusion
Idle timeout using Material-UI with React is easy to implement and could be beneficial to add to your organization’s applications. It’s a very common feature that can help increase your web app security and reduce unnecessary backend API calls.
For the final code base, check it out here, on Github.