1

I created the below HOC which I can use to wrap a React component to add 2 inactivity timers: the first to show the user a warning; the other to log them out. I got the idea from here and it seems to work pretty well. That is, I can add withTimer functionality to a component by wrapping it like this:

export default withTimer(DragDropContext(HTML5Backend)(App));

The problem is the warning alert box halts the event loop (as alert boxes apparently always do), so the logout function is never reached.

I believe a modal (e.g., from react-bootstrap) would solve this, as it presumably would not halt the event loop, thus the logout would occur as intended if the user is still idle after the warning alert.

How would I change the below HOC to use a modal for the warning instead of an alert box? Is this possible? That is, can a HOC that's used to wrap another component include a component itself (i.e., the modal) so as to keep it decoupled from the wrapped component itself?

import React from 'react';
import { Modal } from 'react-bootstrap';

const withTimer = (WrappedComponent) => {
  class WithTimer extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        warningTime: 5000,
        signoutTime: 10000
      };

      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 soon. Click to stay logged in.');
    };

    logout = () => {
      window.alert('You are being logged out!');
      // log the user out here
    };

    render() {
      console.log('HOC');
      return <WrappedComponent {...this.props.children} />;
    }
  }
  return WithTimer;
};

export default withTimer;
Woodchuck
  • 3,869
  • 2
  • 39
  • 70

2 Answers2

2

If you wanted to use a Modal, you could do something like this:

Live Demo

withTimer.js

import React from 'react';
import MyModal from './MyModal';

const withTimer = (WrappedComponent) => {
  class WithTimer extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        warningTime: 5000,
        signoutTime: 10000,
        showModal: false,
        modalMessage: "",
        modalButtonText: "",
      };

      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();
    };

    onModalClick = () => {
      this.setState({
        showModal: false,
      }, () => this.resetTimeout())
    }

    warn = () => {
      this.setState({
        modalButtonText: "Stay Logged In",
        modalHeader: "Warning!",
        modalMessage: 'You will be logged out soon. Click to stay logged in.',
        showModal: true,
      });
    };

    logout = () => {
      this.setState({
        modalButtonText: "Ok",
        modalHeader: "Session Timed Out",
        modalMessage: 'You are being logged out!',
        showModal: true,
      });
      // log the user out here
    };

    render() {
      console.log('HOC');
      return (
        <>
        <MyModal 
          show={this.state.showModal} 
          modalMessage={this.state.modalMessage}
          modalHeader={this.state.modalHeader}
          buttonText={this.state.modalButtonText}
          onButtonClick={this.onModalClick} />
        <WrappedComponent {...this.props.children} />
        </>
      );
    }
  }
  return WithTimer;
};

export default withTimer;

MyModal.js

import React, { useState } from "react";
import { Modal, Button } from "react-bootstrap";

function MyModal({ show = false, modalMessage, modalHeader, onButtonClick, buttonText }) {
  const handleClick = event => {
    onButtonClick(event);
  }

  return (
    <Modal show={show} onHide={handleClick} animation={false}>
      <Modal.Header closeButton>
        <Modal.Title>{modalHeader}</Modal.Title>
      </Modal.Header>
      <Modal.Body>{modalMessage}</Modal.Body>
      <Modal.Footer>
        <Button variant="primary" onClick={handleClick}>
          {buttonText}
        </Button>
      </Modal.Footer>
    </Modal>
  );
}

export default MyModal;
Matt Oestreich
  • 8,219
  • 3
  • 16
  • 41
1

Yes, you can render any components you'd like in the HOC. So in your case you can render a <Modal/>.

Of course, whether the modal is displayed or not is dynamic, so that's a perfect job for the component's state to come into play. Use conditional statements in your render function to either render or not render your modal.

import React from 'react';
import { Modal } from 'react-bootstrap';

const withTimer = (WrappedComponent) => {
  class WithTimer extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        showWarning: false,
        showLogout: false,
        warningTime: 5000,
        signoutTime: 10000
      };

      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 = () => {
      this.setState({ showWarning: true });
    };

    logout = () => {
      this.setState({ showLogout: true });
      // log the user out here
    };

    render() {
      let modal;
      if (this.state.showLogout) {
        modal = <Modal>...</Modal>;
      } else if (this.state.showWarning) {
        modal = <Modal>...</Modal>;
      } else {
        modal = null;
      }

      return <React.Fragment>
        <WrappedComponent {...this.props.children} />
        { modal }
      </React.Fragment>;
    }
  }
  return WithTimer;
};

export default withTimer;
Jacob
  • 77,566
  • 24
  • 149
  • 228
  • Awesome. This looks like the way to go. But for some reason the modal doesn't show up. Do I need to reference it ("modal") in the wrapped component itself somehow? – Woodchuck Oct 15 '19 at 17:56
  • 1
    I'm not familiar with that component, but I'd use React dev tools to see if it's present in the DOM. Might be something like styles (z-index?) or some missing properties of the modal causing it to not render. – Jacob Oct 15 '19 at 17:58
  • 1
    Looks like Matt's answer is actually rendering the dialog, so maybe that'll help you figure it out. – Jacob Oct 15 '19 at 18:00