ReactJS Jest Tesing - fireEvent.click() not triggering a button - reactjs

I am doing testing first time in my life. I have a stopwatch app, and I want to check with test is secondmeter running. I am doing it by triggering the button and check is 00:00:00 still there.
Problem is those 00:00:00 are still in the span after triggering a button. Program working completely fine, not any problem at all. Where is the problem with this triggerin?
I have a button START for that:
class SecondTimer extends React.Component {
constructor(props) {
super(props);
this.state = {
running: false, // Format to false, so when start a program clock is not running
currentTimeMs: 0,
currentTimeSec: 0,
currentTimeMin: 0,
};
}
start = () => {
if (!this.state.running) {
this.setState({ running: true });
this.watch = setInterval(() => this.pace(), 10);
}
};
{this.state.running === false && (
<Button className={"start-stop-reset-nappi"} variant="success" size="lg" onClick=
{this.start}>START</Button>
)}
And I have span where time showing (value before starting is 00:00:00, picture of that below).
<span>
{this.props.formatTime(this.props.currentTimeMin)}:
{this.props.formatTime(this.props.currentTimeSec)}:
{this.props.formatTime(this.props.currentTimeMs)}
</span>
Then I have this test:
import { render, screen, fireEvent } from '#testing-library/react';
import SecondTimer from '../components/SecondTimer.jsx';
test('Test - Secondmeter running or not', () => {
render(<SecondTimer />);
const start = screen.getByText("START") // Here I'll check is there button (text) like START
fireEvent.click(start); // Then I "Click" it
const numbers = screen.getByText(/00:00:00/i) // Check is the 00:00:00 still on screen after triggering the button
expect(numbers).toBeInTheDocument()
});
Complete component code:
import React from 'react';
import SecondTimerNumbers from './SecondTimerNumbers.jsx';
import SecondTimerList from './SecondTimerList.jsx';
import { Button } from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
class SecondTimer extends React.Component {
constructor(props) {
super(props);
// Start funktio määrittää running arvon -> true, joka kertoo sen, että kello käy. Tallentaa tänne ajan, josta SeconTimerNumber hakee tiedon
this.state = {
running: false, // Alustettu falseksi, eli ohjelman avatessa kello ei käy
currentTimeMs: 0,
currentTimeSec: 0,
currentTimeMin: 0,
};
}
// Muuttaa kelloajan numeraaliset luvut stringiksi, sekä lisää yhdet nollat lukuun, jotta luvut ei ns. pompi silmillä.
formatTime = (val, ...rest) => {
let value = val.toString();
if (value.length < 2) {
value = '0' + value;
}
if (rest[0] === 'ms' && value.length < 3) {
value = '0' + value;
}
return value;
};
// Start napista painalla käynnistää tämän funktion. Muuttaa state running trueksi, eli kello käy. SetInterval on Reactin oma sekuntikello, ja this.pace 10 määrittelee sadasosasekunnit
start = () => {
if (!this.state.running) {
this.setState({ running: true });
this.watch = setInterval(() => this.pace(), 10);
}
};
// Stop painikkeesta muuttaa running staten falseksi -> kello pysähtyy, sekä tyhjentää sekuntikellon (pysäyttää).
stop = () => {
this.setState({ running: false });
clearInterval(this.watch);
};
// Määrittelee kellon toiminnan
pace = () => {
this.setState({ currentTimeMs: this.state.currentTimeMs + 10 }); // Lisää 10ms kerrallaan
// Kun millisekunteja 1000, lisää yhden sekunnin, sekä muuttaa millisekuntien arvoksi 0 kierroksen jälkeen
if (this.state.currentTimeMs >= 1000) {
this.setState({ currentTimeSec: this.state.currentTimeSec + 1 });
this.setState({ currentTimeMs: 0 });
}
// Kun sekunteja on 60, lisää yhden minuutin, sekä muuttaa sekuntien arvoksi 0 kierroksen jälkeen
if (this.state.currentTimeSec >= 60) {
this.setState({ currentTimeMin: this.state.currentTimeMin + 1 });
this.setState({ currentTimeSec: 0 });
}
};
// Määrittää (resetoi) staten allaolevat arvot nollaksi, jolloin sekuntikello voidaan taas aloittaa alusta
resetTime = () => {
this.setState({
currentTimeMs: 0,
currentTimeSec: 0,
currentTimeMin: 0,
});
};
// UserInterface eli käyttöliittymä renderöityy tässä
render() {
return (
<div className={'main-div'}>
<div className={'left'}>
{this.state.running === false && (
<Button className={"start-stop-reset-nappi"} variant="success" size="lg" onClick={this.start}>START</Button>
)}
{this.state.running === true && (
<Button className={'start-stop-reset-nappi'} variant="warning" size="lg" onClick={this.stop}>PAUSE</Button>
)}
<Button className={'start-stop-reset-nappi'} variant="danger" size="lg" onClick={this.resetTime}>RESET</Button>
<div className={'tulostaulu-main'}>
<SecondTimerNumbers
ref="display"
{...this.state}
formatTime={this.formatTime}
/>
</div>
</div>
<div className={'right'}>
<SecondTimerList clickResetTime={this.resetTime} {...this.state} formatTime={this.formatTime} />
</div>
</div>
);// Yllä oleva ClickResetTime funktio on tämän sivut resetTime funktio.
// Tällä saan kutsuttua funktiota toisesta componentista, tavallaan "injektoin" funktion tähän componenttiin.
}
}
export default SecondTimer;

I don't really know what is your problem with your test. I've created a small test based on your current one which works really well.
The idea of the test is pretty simple. As I clicked the button, I expect to see the change in seconds. The key thing is to use waitFor api with a specified timeout option to make sure testing library resolving your UI change.
Here is the snippet:
import React from "react";
import { render, screen, fireEvent, waitFor } from "#testing-library/react";
import "#testing-library/jest-dom";
import { SecondTimer } from "./App";
test("should work", async () => {
render(<SecondTimer />);
const start = screen.getByText("START");
fireEvent.click(start);
await waitFor(() => screen.getByText(/00:02:00/i), {
timeout: 2000 // wait 2s
});
const numbers = screen.getByText(/00:02:00/i);
expect(numbers).toBeInTheDocument();
});
Here is the codesanbox which I've created exclusively for you (The setup to make the test able to run is based on create-react-app script):
https://codesandbox.io/s/morning-browser-92uks?file=/src/App.test.js
NOTE: To run the test, you click a Tests tab next to Browser on the preview area.

Related

Disabling button based on child component state in React

I am using a countdown component as a child component.
I want to disable/reable a button based on the state value of the counter, but I can't seem to read the value correctly.
This is what I have tried.
This is the countdown component:
import React from "react";
import PropTypes from "prop-types";
export default class Counter extends React.Component {
constructor() {
super();
this.state = { time: {}, seconds: 15 };
this.timer = 0;
this.startTimer = this.startTimer.bind(this);
this.countDown = this.countDown.bind(this);
}
secondsToTime(secs){
let hours = Math.floor(secs / (60 * 60));
let divisor_for_minutes = secs % (60 * 60);
let minutes = Math.floor(divisor_for_minutes / 60);
let divisor_for_seconds = divisor_for_minutes % 60;
let seconds = Math.ceil(divisor_for_seconds);
let obj = {
"h": hours,
"m": minutes,
"s": seconds
};
return obj;
}
componentDidMount() {
let timeLeftVar = this.secondsToTime(this.state.seconds);
this.setState({ time: timeLeftVar });
}
startTimer() {
if (this.timer === 0 && this.state.seconds > 0) {
this.timer = setInterval(this.countDown, 1000);
} else if ((this.timer === 0 && this.state.seconds === 0)){
this.state.seconds = 15;
this.timer = setInterval(this.countDown, 1000);
}
}
countDown() {
// Remove one second, set state so a re-render happens.
let seconds = this.state.seconds - 1;
this.setState({
time: this.secondsToTime(seconds),
seconds: seconds,
});
// Check if we're at zero.
if (seconds === 0) {
clearInterval(this.timer);
this.timer = 0;
console.log("counter is 0");
console.log(this.state.seconds);
console.log(this.timer);
}
}
render() {
this.startTimer();
return(
<span className={
this.state.seconds === 0 ? 'timerHidden' : 'timerActive'
}>
({this.state.time.s})
</span>
);
}
}
And how I read it and reset it in the parent component:
import Counter from '../Counter/Counter.js';
export default class Verify extends React.Component {
state = {
username: this.username,
email: this.email,
code: ""
};
constructor(props) {
super(props);
this.child = React.createRef();
}
resetTimer = () => {
this.child.current.startTimer();
};
resendConfirmationCode = async e =>{
this.resetTimer();
...
}
return (
<button
className="btn btn-primary register empty"
type="button"
disabled={this.child.current.seconds > 0}
onClick={this.resendConfirmationCode}>Resend code <Counter ref={this.child}/>
</button>
);
Inserting the counter works fine, reseting also, but the disabling of the button throws the following error:
TypeError: Cannot read property 'seconds' of null
Verify.render
> 109 | disabled={this.child.current.seconds > 0}
The this.child ref will be null/undefined on the initial render. Since you probably also want to disable the button if the counter component isn't available for some reason, you can just check if the ref's current value is falsey or if it is truthy and state.seconds of the child greater than 0.
<button
...
disabled={!this.child.current || this.child.current.state.seconds > 0}
onClick={this.resendConfirmationCode}
>
Resend code
</button>
<Counter ref={this.child} />
If we invert the second condition we can combine them into a single comparison using Optional Chaining.
<button
...
disabled={!this.child.current?.state.seconds <= 0}
onClick={this.resendConfirmationCode}
>
Resend code
</button>
<Counter ref={this.child} />

Having trouble making a prop update dynamically in countdown timer - create with a react hook

I'm new to React and building a project that requires dynamically setting a countdown timer from a parent component. I found a react countdown timer online that uses a hook, but I'm not too familiar hooks yet.
Toward the bottom of my code, you can see a parent class where I'm passing 'cycleTimeSelected' to the Countdown component/hook. It works correctly up to that point.
But I'm not having success getting it to update the timer correctly and dynamically. timeRemaining in React.useState() is the variable I need to update. It doesn't work if I use props.cycleTimeSelected directly. I think I understand why that is, so I tried to use componentWillRecieveProps to set the state, but that's not working.
I'm think I'm confused about React.useState and how that relates to setState for one thing. Can anyone spot my problem here?
import React, { Component } from 'react'
import { PausePresentation, SkipNext, Stop, PlayCircleOutline} from '#material-ui/icons';
import './Timer.css'
function Countdown(props) {
const [timer, setTimer] = React.useState({
name: 'timer',
isPaused: true,
time: 100,
timeRemaining: props.cycleTimeSelected,
timerHandler: null
})
//const componentWillReceiveProps = nextProps => {
//this.setState({ timeRemaining: nextProps.cycleTimeSelected });
//}
const handleTimeChange = e => {
setTimer({
...timer,
time: props.cycleTimeSelected,
timeRemaining: Number(e.target.value),
})
}
React.useEffect(() => {
if (timer.timeRemaining === 0) {
clearInterval(timer.timerHandler)
}
}, [timer.timeRemaining, timer.timerHandler])
const updateTimeRemaining = e => {
setTimer(prev => {
return { ...prev, timeRemaining: prev.timeRemaining - 1 }
})
}
const handleStart = e => {
const handle = setInterval(updateTimeRemaining, 1000);
setTimer({ ...timer, isPaused: false, timerHandler: handle })
}
const handlePause = e => {
clearInterval(timer.timerHandler)
setTimer({ ...timer, isPaused: true })
}
return <React.Fragment>
{/* { <input value={props.cycleTimeSelected} type="" onChange={handleTimeChange} /> } */}
{timer.name && timer.time && <div className={timer.timeRemaining === 0 ? 'time-out':''}>
{timer.isPaused && <div className="floatLeft"><i className="material-icons pause"><PlayCircleOutline onClick={handleStart}></PlayCircleOutline></i></div>}
{!timer.isPaused && <div className="floatLeft"><i className="material-icons pause"><PausePresentation onClick={handlePause}></PausePresentation></i></div>}
{`Remaining: ${timer.timeRemaining}`}
</div>}
</React.Fragment>
}
class Timer extends React.Component {
render(){
return(
<>
<div className="floatLeft"><i className="material-icons bottom-toolbar stop"><Stop onClick={this.clickStop}></Stop></i></div>
<div className="floatLeft"><i className="material-icons bottom-toolbar skip_next"><SkipNext onClick={this.clickSkip}></SkipNext></i></div>
<div className="floatLeft"><div id="timer"><Countdown cycleTimeSelected={this.props.cycleTimeSelected}></Countdown></div></div>
</>
);
}
}
If you want timer.timeRemaining in Countdown to update via props then you should implement an effect with a dependency on props.cycleTimeSelected. useEffect is nearly the functional component equivalent to a class-based component's componentDidMount, componentDidUpdate, and componentWillUnmount lifecycle functions. We're interested in the "update" lifecycle.
useEffect(() => {
setTimer((timer) => ({
...timer, // copy existing state
timeRemaining: props.cycleTimeSelected // update property
}));
}, [props.cycleTimeSelected]);

Clear Interval not working in my component JavaScript?

I have a component that will start counting down from 5 mins when a timestamp (obsTakenTime) is received via props. When the countdown gets <=0, I render ‘Overdue’. At this point I need to clear interval which I think I've done, the issues is when if, I refresh the page the obstimeleft should remain overdue but the countdown automatically starts from 59 mins because the value of nextObservationTime becomes 59min and this.state.obsTimeleft becomes undefined even thought the value of timestamp obsTakenTime is the same. I've looked at other similar threads on SO but I couldn't get mine to work. Any help is appreciated.
similar post - Clear interval in React class
Countdown component
export default class ObservationCountDown extends React.Component {
constructor(props) {
super(props);
this.state = {
obsTimeleft: undefined
};
this.countDown = this.countDown.bind(this);
this.startCounter = this.startCounter.bind(this);
this.countDownInterval = null;
}
countDown() {
const { obsTakenTime} = this.props; //when last obs was taken by the user in ms
const nextDueTimeForObs = moment(obsTakenTime).add(5, 'minutes');
const nextObservationTime = Number(nextDueTimeForObs.subtract(moment.now()).format('m'));
const timeToDisable = 2; // disable buttons time
this.setState({ obsTimeleft: nextObservationTime + ' min' }, () => {
if (nextObservationTime <= Number(timeToDisable)) {
this.props.disablePatientUpdate();
}
if (nextObservationTime <= 0) {
clearInterval(this.countDownInterval); // doesn't work here
this.setState({ obsTimeleft: 'Overdue' }, () => {
if(this.state.obsTimeleft === 'Overdue'){
clearInterval(this.countDownInterval); // doesn't work here
}
});
}
});
}
componentDidMount() {
this.startCounter();
}
startCounter() {
this.countDownInterval = setInterval(this.countDown, 1000);
}
componentDidUpdate(prevProps){
if(this.props.patient.timestamp !== prevProps.patient.timestamp){
this.startCountdown();
}
}
componentWillUnmount(){
clearInterval(this.countDownInterval);
}
render() {
const { obsTimeleft } = this.state;
return (
<>
{(obsTimeleft && obsTimeleft === 'Overdue') ?
<div className="text-danger">
<strong>{obsTimeleft}</strong>
</div> :
<div>
<strong>{.obsTimeleft}</strong>
</div>}
</>
);
}
}
another version of countDown() that I tried and didn't work
countDown() {
const { obsTakenTime } = this.props; // obs duration - when last obs was taken by the user in min
const nextDueTimeForObs = moment(obsTakenTime).add(2, 'minutes');
const nextObservationTime = Number(nextDueTimeForObs.subtract(moment.now()).format('m'));
console.log('nextObservationTime', nextObservationTime);
this.setState({ obsTimeleft: nextObservationTime + ' min' })
if (nextObservationTime <= 0) {
this.setState({ obsTimeleft: 'Overdue' }, () => {
if(this.state.obsTimeleft === 'Overdue') {
clearInterval(this.countDownInterval);
}
});
this.props.enablePatientUpdate();
this.props.resetPatient(patient);
}
}

Deck.gl how to show popup onClick

Setup:
Basic react app using react-map-gl to show a map with a deck.gl ScatterplotLayer over the top to visualise the data
Goal:
1) To show points on a map as circles of a given radius and colour.
2) When a user clicks on a circle, a tooltip/popup should show with more data about it (included in the data provided) until the user clicks away (essentially the same as this graph but for click instead of hover, http://uber.github.io/deck.gl/#/documentation/layer-catalog/scatterplot-layer. FYI I looked at the code for this and the hover logic has been removed, I assume for simplicity).
Issue:
I have completed point 1 but I cannot get point 2 to work. The furthest I have gotten to prove the data is there is to log to the console.
To note:
I'm not married to react-tooltip - I don't mind taking it out entirely if there's a better way of doing this. I only need to keep mapbox and deck.gl.
Data: https://gist.github.com/NikkiChristofi/bf79ca37028b29b50cffb215360db999
deckgl-overlay.js
import React, {Component} from 'react';
import ReactTooltip from 'react-tooltip';
import DeckGL, {ScatterplotLayer} from 'deck.gl';
export default class DeckGLOverlay extends Component {
static get defaultViewport() {
return {
longitude: 0,
latitude: 0,
zoom: 2,
maxZoom: 16,
pitch: 0,
bearing: 0
};
}
# in this method I want to update the variable tooltipText with
# whatever object data has been clicked.
# The console log successfully logs the right data (i.e. the third
# element in the array), but the tooltip doesn't even show
onClickHandler = (info) => {
let dataToShow = info ? info.object[2] : "not found";
this.tooltipText = dataToShow;
console.log(dataToShow);
}
render() {
const {viewport, lowPerformerColor, highPerformerColor, data, radius, smallRadius, largeRadius} = this.props;
if (!data) {
return null;
}
const layer = new ScatterplotLayer({
id: 'scatter-plot',
data,
radiusScale: radius,
radiusMinPixels: 0.25,
getPosition: d => [d[1], d[0], 0],
getColor: d => d[2] > 50 ? lowPerformerColor : highPerformerColor,
getRadius: d => d[2] < 25 || d[2] > 75 ? smallRadius : largeRadius,
updateTriggers: {
getColor: [lowPerformerColor, highPerformerColor]
},
pickable: true,
onClick: info => this.onClickHandler(info),
opacity: 0.3
});
return (
<DeckGL {...viewport} layers={ [layer] } data-tip={this.tooltipText}>
<ReactTooltip />
</DeckGL>
);
}
}
app.js
import React, {Component} from 'react';
import {render} from 'react-dom';
import MapGL from 'react-map-gl';
import DeckGLOverlay from './deckgl-overlay.js';
import {json as requestJson} from 'd3-request';
const MAPBOX_TOKEN = process.env.MAPBOX_TOKEN; // eslint-disable-line
const lowPerformerColor = [204, 0, 0];
const highPerformerColor = [0, 255, 0];
const smallRadius = 500;
const largeRadius = 1000;
const DATA_URL = 'https://gist.github.com/NikkiChristofi/bf79ca37028b29b50cffb215360db999';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
viewport: {
...DeckGLOverlay.defaultViewport,
width: 500,
height: 500
},
data: null
};
requestJson(DATA_URL, (error, response) => {
if (!error) {
console.log(response);
this.setState({data: response});
}
else{
console.log(error);
}
});
}
componentDidMount() {
window.addEventListener('resize', this._resize.bind(this));
this._resize();
}
_resize() {
this._onViewportChange({
width: window.innerWidth,
height: window.innerHeight
});
}
_onViewportChange(viewport) {
this.setState({
viewport: {...this.state.viewport, ...viewport}
});
}
render() {
const {viewport, data} = this.state;
return (
<MapGL
{...viewport}
onViewportChange={this._onViewportChange.bind(this)}
mapboxApiAccessToken={MAPBOX_TOKEN}
mapStyle='mapbox://styles/mapbox/dark-v9'>
<DeckGLOverlay viewport={viewport}
data={data}
lowPerformerColor={lowPerformerColor}
highPerformerColor={highPerformerColor}
smallRadius={smallRadius}
largeRadius={largeRadius}
radius={300}
/>
</MapGL>
);
}
}
Figured out a way to do it.
Solution
I bubbled up the onClick event to the MapGL layer, and used the Popup element to display the data.
so in app.js:
1) import the Popup element from react-map-gl
import MapGL, { Popup } from 'react-map-gl';
2) Set coordinates state and "info" (to show in the popup)
constructor(props) {
super(props);
this.state = {
viewport: {
...DeckGLOverlay.defaultViewport,
width: 500,
height: 500
},
data: null,
coordinates: [-0.13235092163085938,51.518250335096376],
info: "Hello"
};
3) Create callback method that sets the state with the new data (info will just be an element from the data, can be anything you want to display in the popup though)
myCallback = (info) => {
console.log(info);
if(info){
this.setState({coordinates: info.lngLat, info: info.object[2]});
}
}
4) Render the popup and reference the callback method in the DeckGL layer
return (
<MapGL
{...viewport}
{...this.props}
onViewportChange={this._onViewportChange.bind(this)}
mapboxApiAccessToken={MAPBOX_TOKEN}
mapStyle='mapbox://styles/mapbox/dark-v9'>
<Popup
longitude={this.state.coordinates[0]}
latitude={this.state.coordinates[1]}>
<div style={style}>
<p>{this.state.info}</p>
</div>
</Popup>
<DeckGLOverlay viewport={viewport}
data={data}
lowPerformerColor={lowPerformerColor}
highPerformerColor={highPerformerColor}
smallRadius={smallRadius}
largeRadius={largeRadius}
radius={300}
callbackFromParent={this.myCallback}
/>
</MapGL>
);
and in deckgl-overlay.js:
1) Feed data information into the parent's (app.js) method
onClick: info => this.props.callbackFromParent(info),
(obviously delete the React-tooltip element and onClick event handler in deckoverlay.js to clean up)
For anyone reading this who wants to use a custom popover or one from a third party library like antd that doesn't support exact position as a prop I got around this problem by just creating a <div style={{ position: 'absolute', left: x, top: y}} /> to act as a child node for the popover to reference. X and Y are initially set to 0:
const [selectedPoint, setSelectedPoint] = useState({});
const [x, setX] = useState(0);
const [y, setY] = useState(0);
and then are set onClick in the GeoJsonLayer:
const onClick = ({ x, y, object }) => {
setSelectedPoint(object);
setX(x);
setY(y);
};
const layer = new GeoJsonLayer({
id: "geojson-layer",
data,
pickable: true,
stroked: false,
filled: true,
extruded: true,
lineWidthScale: 20,
lineWidthMinPixels: 2,
getFillColor: [0, 0, 0, 255],
getRadius: 50,
getLineWidth: 1,
getElevation: 30,
onClick
});
The downside to this approach is that the popover won't stay with the point if the map is zoomed/panned because X and Y are viewport coordinates vs lat and long.

Session timeout warning modal using react

I have a requirement to display timeout warning modal after 13 mins of inactivity and end session after 15 mins if user takes no action. I need to achieve this using reactjs. I checked react-timeout at https://www.npmjs.com/package/react-timeout#react-classic-verbose, but that didn't help.
If anyone knows of a way to do this, please share with me.
You can create a higher order component like this and can pass child component through higher order component
HOC:
`// code
export default function(ComposedClass) {
class AutoLogout extends React.Component {
constructor(props) {
super(props);
this.state = {
warningTime: 1000 * 60 * 10,
signoutTime: 1000 * 60 * 15,
};
}
componentDidMount() {
this.events = [
'load',
'mousemove',
'mousedown',
'click',
'scroll',
'keypress'
];
for (var i in this.events) {
window.addEventListener(this.events[i], this.resetTimeout);
}
this.setTimeout();
}
clearTimeoutFunc = () => {
if (this.warnTimeout) clearTimeout(this.warnTimeout);
if (this.logoutTimeout) clearTimeout(this.logoutTimeout);
};
setTimeout = () => {
this.warnTimeout = setTimeout(this.warn, this.state.warningTime);
this.logoutTimeout = setTimeout(this.logout, this.state.signoutTime);
};
resetTimeout = () => {
this.clearTimeoutFunc();
this.setTimeout();
};
warn = () => {
window.alert("You will be logged out automatically in 1 minute")
console.log('You will be logged out automatically in 1 minute.');
};
logout = () => {
// Send a logout request to the API
console.log('Sending a logout request to the API...');
this.destroy();
};
destroy = () => {
//clear the session
browserHistory.push('/');
window.location.assign('/');
};
render() {
return (
<div>
<ComposedClass {...this.props} />
</div>
);
}
}
}
`
You can wrap this HOC to all those component in which you want to give user warning due to inactivity, in routing file
<Route path="/test" component={HOC(comonent)} />
in above code component will be the page where you want to add this functionality.
If you want to achieve same with Package, then you can write below code using React Idle Timer Package
npm i react-idle-timer
import React from 'react'
import { useIdleTimer } from 'react-idle-timer'
import { useHistory } from 'react-router'
const SESSION_IDEL_MINUTES = 4;
const AutoLagoutTimer = (props: any) => {
const { ComposedClass } = props
const history = useHistory()
const handleOnIdle = (event: any) => {
// SHOW YOUR MODAL HERE AND LAGOUT
console.log('user is idle', event)
console.log('last active', getLastActiveTime())
history.push("/lagout")
}
const {getLastActiveTime } = useIdleTimer({
timeout: 1000 * 60 * SESSION_IDEL_MINUTES,
onIdle: handleOnIdle,
debounce: 500,
})
return <ComposedClass />
}
export default AutoLagoutTimer;
And for all your protected routes you can wrap with this component like below
<Route path={"/dashboard"}>
<AutoLagoutTimer ComposedClass={Dashboard} />
</Route>
Here is a full example of the modal idle warning.
Assuming you are using reactstrap for styling here is the example.
import React, {useState, useEffect} from 'react';
import {Modal, ModalHeader, ModalBody, ModalFooter} from 'reactstrap';
function IdleMonitor()
{
//Modal
const [idleModal, setIdleModal] = useState(false);
let idleTimeout = 1000 * 60 * 1; //1 minute
let idleLogout = 1000 * 60 * 2; //2 Minutes
let idleEvent;
let idleLogoutEvent;
/**
* Add any other events listeners here
*/
const events = [
'mousemove',
'click',
'keypress'
];
/**
* #method sessionTimeout
* This function is called with each event listener to set a timeout or clear a timeout.
*/
const sessionTimeout = () =>
{
if(!!idleEvent) clearTimeout(idleEvent);
if(!!idleLogoutEvent) clearTimeout(idleLogoutEvent);
idleEvent = setTimeout(() => setIdleModal(true), idleTimeout); //show session warning modal.
idleLogoutEvent = setTimeout(() => logOut, idleLogout); //Call logged out on session expire.
};
/**
* #method extendSession
* This function will extend current user session.
*/
const extendSession = () =>
{
console.log('user wants to stay logged in');
}
/**
* #method logOut
* This function will destroy current user session.
*/
const logOut = () =>
{
console.log('logging out');
}
useEffect(() =>
{
for (let e in events)
{
window.addEventListener(events[e], sessionTimeout);
}
return () =>
{
for(let e in events)
{
window.removeEventListener(events[e], sessionTimeout);
}
}
},[]);
return (
<Modal isOpen={idleModal} toggle={() => setIdleModal(false)}>
<ModalHeader toggle={() => setIdleModal(false)}>
Session expire warning
</ModalHeader>
<ModalBody>
your session will expire in {idleLogout / 60 / 1000} minutes. Do you want to extend the session?
</ModalBody>
<ModalFooter>
<button className="btn btn-info" onClick={()=> logOut()}>Logout</button>
<button className="btn btn-success" onClick={()=> extendSession()}>Extend session</button>
</ModalFooter>
</Modal>
)
}
export default IdleMonitor;
after you create that class you can simply call it from anywhere like this
<IdleMonitor/>
don't forget to import it.

Resources