Search code examples
javascriptreactjsreact-bootstrap

Include Modal functionality in React Higher-Order Component


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;

Solution

  • 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;