Skip to content

Instantly share code, notes, and snippets.

@mknabe
Last active September 18, 2021 09:14
Show Gist options
  • Star 38 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save mknabe/bfcb6db12ef52323954a28655801792d to your computer and use it in GitHub Desktop.
Save mknabe/bfcb6db12ef52323954a28655801792d to your computer and use it in GitHub Desktop.
React component to warn users about unsaved changes to forms when they attempt navigate away from the page using redux-form

Use with the following

  • react-router
  • redux-form

You must use this compontent on the route component where the form is shown not on the form component itself. This is because this.props.route is only added to route components, and I didn't want to pass that down through props.

Inspired by this Codepad

Feedback appreciated

To Do

  • Accept an array of form names
export default warnAboutUnsavedChanges(
connect(
mapStateToProps,
mapDispatchToProps
)(Component),
'clientContactsForm'
);
import React, {PropTypes} from 'react';
import {withRouter} from 'react-router';
import {connect} from 'react-redux';
import {isDirty} from 'redux-form';
function warnAboutUnsavedChanges(WrappedComponent, formName) {
class WarnAboutUnsavedChanges extends React.Component {
componentDidUpdate() {
this._promptUnsavedChange(this.props.isFormDirty);
}
componentWillUnmount() {
window.onbeforeunload = null;
}
_promptUnsavedChange(isUnsaved = false, leaveMessage = 'Leave with unsaved change?') {
// Detecting page transition (prevent leaving by setting true)
// for React Router version > 2.4
this.props.router.setRouteLeaveHook(this.props.route, () => isUnsaved && confirm(leaveMessage));
// Detecting browser close
window.onbeforeunload = isUnsaved && (() => leaveMessage);
}
render() {
return <WrappedComponent {...this.props} />;
}
}
WarnAboutUnsavedChanges.propTypes = {
isFormDirty: PropTypes.bool,
// react-router
router: PropTypes.object,
route: PropTypes.object
};
const mapStateToProps = (state) => ({
isFormDirty: isDirty(formName)(state)
});
return withRouter(connect(
mapStateToProps,
null
)(WarnAboutUnsavedChanges));
}
export default warnAboutUnsavedChanges;
@dpwrussell
Copy link

dpwrussell commented Jun 19, 2017

Exactly what I was looking for without having to code it myself. Thanks.

Just a suggestion, but I think this would be a really useful module to have in npm. redux-form-navwarning or something.

Although actually it's specific to react-router, which would be nicer if it was configurable.

@acmh
Copy link

acmh commented Sep 11, 2017

Hi, can you give me a more detailed usage example? When I try to use, I'm receiving router undefined error. Thank you for your time!

@EdsonAlcala
Copy link

yeah, I cannot make it work, can you explain better?

@MaxiSantos
Copy link

It's working, somehow, my component is aborting the new render of the new page but the url is changed. What version of the router are u using?

@kavimaluskam
Copy link

Guess should change the below lines of code (as they are required on my side):

_promptUnsavedChange(isUnsaved = false, leaveMessage = 'Leave with unsaved change?') {
  // Detecting page transition (prevent leaving by setting true)
  // for React Router version > 2.4
  this.props.router.setRouteLeaveHook(this.props.route, () => isUnsaved ? confirm(leaveMessage) : true);

  // Detecting browser close
  window.onbeforeunload = isUnsaved ? (() => leaveMessage) : true;
}

@filipenevola
Copy link

filipenevola commented Jul 18, 2018

Hello, just sharing my HOC inspired by this one 😉

A few changes:

  • Using anyTouched and isSubmittingForm in combination with isDirty
  • Removing the hooks, maybe that is not needed with the latest React Router but I'm using 2.4.0 and that was necessary
// withWarnUnsavedForm.js
import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'recompose';
import { withRouter } from 'react-router'; // 2.4.0
import { connect } from 'react-redux';
import { isDirty } from 'redux-form';
 
const LEAVE_MESSAGE = 'Any unsaved changes will be lost. Are you sure?';
 
export const withWarnUnsavedForm = ({
  form,
  leaveMessage = LEAVE_MESSAGE,
}) => WrappedComponent => {
  class WarnUnsavedForm extends React.Component {
    // eslint-disable-next-line react/sort-comp
    removeSetRouteLeaveHook = null;

    componentDidUpdate() {
      const {
        router,
        route,
        isDirtyForm,
        isSubmittingForm,
        anyTouched,
      } = this.props;
      if (!router || !router.setRouteLeaveHook || !route) {
        // eslint-disable-next-line no-console
        console.warn('Incorrect usage of withWarnUnsavedForm, missing props', {
          router,
          route,
          isDirtyForm,
          isSubmittingForm,
          anyTouched,
        });
        return;
      }

      if (this.removeSetRouteLeaveHook) {
        this.removeSetRouteLeaveHook();
      }
      this.removeSetRouteLeaveHook = router.setRouteLeaveHook(route, () => {
        // eslint-disable-next-line no-alert
        const leave =
          isDirtyForm && !isSubmittingForm && anyTouched
            ? confirm(leaveMessage)
            : true;
        if (this.removeSetRouteLeaveHook && leave) {
          this.removeSetRouteLeaveHook();
        }
        return leave;
      });
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  }

  WarnUnsavedForm.propTypes = {
    router: PropTypes.object.isRequired,
    route: PropTypes.object.isRequired,
    isDirtyForm: PropTypes.bool,
    isSubmittingForm: PropTypes.bool,
    anyTouched: PropTypes.bool,
  };

  return compose(
    withRouter,
    connect(state => ({
      isDirtyForm: isDirty(form)(state),
      isSubmittingForm: isSubmitting(form)(state),
    }))
  )(WarnUnsavedForm);
};

I have also created a new HOC to use directly instead of the default reduxForm HOC

  • With defaults for when considering touch
  • A way to disable withWarnUnsavedForm
// withReduxForm.js
import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'recompose';
import { reduxForm } from 'redux-form';
import { withWarnUnsavedForm } from '../with-warn-unsaved-form/withWarnUnsavedForm';

export const withReduxForm = ({
  disableWarnUnsavedForm = false,
  touchOnChange = true,
  touchOnBlur = false,
  withWarnUnsavedFormProps = {},
  ...reduxFormProps
}) => WrappedComponent => {
  const WithReduxForm = props => <WrappedComponent {...props} />;

  WithReduxForm.propTypes = {
    form: PropTypes.string.isRequired,
    touchOnChange: PropTypes.bool,
    touchOnBlur: PropTypes.bool,
    disableWarnUnsavedForm: PropTypes.bool,
    withWarnUnsavedFormProps: PropTypes.object,
  };

  const reduxFormHOC = reduxForm({
    ...reduxFormProps,
    touchOnChange,
    touchOnBlur,
  });

  if (disableWarnUnsavedForm) {
    return reduxFormHOC(WithReduxForm);
  }

  return compose(
    reduxFormHOC,
    withWarnUnsavedForm({
      ...withWarnUnsavedFormProps,
      form: reduxFormProps.form,
    })
  )(WithReduxForm);
};

@vjekooo
Copy link

vjekooo commented Jul 27, 2018

Anything similar to this for Router v.4?

@prakashsvmx
Copy link

+1

@aishek
Copy link

aishek commented Aug 30, 2018

My version for Router v.4

import React, { Fragment } from "react"
import PropTypes from "prop-types"
import { withRouter, Prompt } from "react-router"
import { connect } from "react-redux"
import { isDirty } from "redux-form"

function warnAboutUnsavedForm(WrappedComponent, formName) {
  class WarnAboutUnsavedChanges extends React.Component {
    static propTypes = {
      isFormDirty: PropTypes.bool,
      leaveMessage: PropTypes.string.isRequired
    }

    static defaultProps = {
      leaveMessage: "Leave with unsaved change?"
    }

    componentDidUpdate() {
      this._promptUnsavedChange(this.props.isFormDirty, this.props.leaveMessage)
    }

    componentWillUnmount() {
      window.onbeforeunload = null
    }

    _promptUnsavedChange(isUnsaved = false, leaveMessage) {
      window.onbeforeunload = isUnsaved && (() => leaveMessage)
    }

    render() {
      return (
        <Fragment>
          <WrappedComponent {...this.props} />
          <Prompt
            when={this.props.isFormDirty}
            message={this.props.leaveMessage}
          />
        </Fragment>
      )
    }
  }

  const mapStateToProps = state => ({
    isFormDirty: isDirty(formName)(state)
  })

  return withRouter(connect(mapStateToProps, null)(WarnAboutUnsavedChanges))
}

export default warnAboutUnsavedForm

@davidfurlong
Copy link

These solutions don't seem to handle navigation post submitting state/during form submit

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment