简体   繁体   中英

How to use a custom component with react-router route transitions?

The article Confirming Navigation explains how to use a browser confirmation box in your transition hook. Fine. But I want to use my own Dialog box. If I were to use the methods from the history module I think this is possible. Is it possible to do this with the setRouteLeaveHook in react-router?

The core problem is that setRouteLeaveHook expects the hook function to return its result synchronously . This means you don't have the time to display a custom dialog component, wait for the user to click an option, and then return the result. So we need a way to specify an asynchronous hook. Here's a utility function I wrote:

// Asynchronous version of `setRouteLeaveHook`.
// Instead of synchronously returning a result, the hook is expected to
// return a promise.
function setAsyncRouteLeaveHook(router, route, hook) {
  let withinHook = false
  let finalResult = undefined
  let finalResultSet = false
  router.setRouteLeaveHook(route, nextLocation => {
    withinHook = true
    if (!finalResultSet) {
      hook(nextLocation).then(result => {
        finalResult = result
        finalResultSet = true
        if (!withinHook && nextLocation) {
          // Re-schedule the navigation
          router.push(nextLocation)
        }
      })
    }
    let result = finalResultSet ? finalResult : false
    withinHook = false
    finalResult = undefined
    finalResultSet = false
    return result
  })
}

Here is an example of how to use it, using vex to show a dialog box:

componentWillMount() {
  setAsyncRouteLeaveHook(this.context.router, this.props.route, this.routerWillLeave)
}
​
routerWillLeave() {
  return new Promise((resolve, reject) => {
    if (!this.state.textValue) {
      // No unsaved changes -- leave
      resolve(true)
    } else {
      // Unsaved changes -- ask for confirmation
      vex.dialog.confirm({
        message: 'There are unsaved changes. Leave anyway?' + nextLocation,
        callback: result => resolve(result)
      })
    }
  })
}

I made it work by setting a boolean on state whether you have confirmed to navigate away (using react-router 2.8.x). As it says in the link you posted: https://github.com/ReactTraining/react-router/blob/master/docs/guides/ConfirmingNavigation.md

return false to prevent a transition w/o prompting the user

However, they forget to mention that the hook should be unregistered as well, see here and here .

We can use this to implement our own solution as follows:

class YourComponent extends Component {
  constructor() {
    super();

    const {route} = this.props;
    const {router} = this.context;

    this.onCancel = this.onCancel.bind(this);
    this.onConfirm = this.onConfirm.bind(this);

    this.unregisterLeaveHook = router.setRouteLeaveHook(
      route,
      this.routerWillLeave.bind(this)
    );
  }

  componentWillUnmount() {
    this.unregisterLeaveHook();
  }

  routerWillLeave() {
    const {hasConfirmed} = this.state;
    if (!hasConfirmed) {
      this.setState({showConfirmModal: true});

      // Cancel route change
      return false;
    }

    // User has confirmed. Navigate away
    return true;
  }

  onCancel() {
    this.setState({showConfirmModal: false});
  }

  onConfirm() {
    this.setState({hasConfirmed: true, showConfirmModal: true}, function () {
      this.context.router.goBack();
    }.bind(this));
  }

  render() {
    const {showConfirmModal} = this.state;

    return (
      <ConfirmModal
        isOpen={showConfirmModal}
        onCancel={this.onCancel}
        onConfirm={this.onConfirm} />
    );
  }
}

YourComponent.contextTypes = {
  router: routerShape
};

Posting my solution for intercept back button or even a route change. This works with React-router 2.8 or higher. Or even with withRouter

import React, {PropTypes as T} from 'react';

...
componentWillMount() {
        this.context.router.setRouteLeaveHook(this.props.route, this.routerWillLeaveCallback.bind(this));
    }

    routerWillLeaveCallback(nextLocation) {
        let showModal = this.state.unsavedChanges;
        if (showModal) {
            this.setState({
                openUnsavedDialog: true,
                unsavedResolveCallback: Promise.resolve
            });
            return false;
        }
        return true;
    }
}


YourComponent.contextTypes = {
    router: T.object.isRequired
};

Above is great except when user goes back in history. Something like the following should fix the problem:

if (!withinHook && nextLocation) {
    if (nextLocation.action=='POP') {
        router.goBack()
    } else {
      router.push(nextLocation)
    }
}

Here's my solution to the same. I made a custom dialog component that you can use to wrap any component in your app. You can wrap your header and this way have it present on all pages. It assumes you're using Redux Form, but you can simply replace areThereUnsavedChanges with some other form change checking code. It also uses React Bootstrap modal, which again you can replace with your own custom dialog.

import React, { Component } from 'react'
import { connect } from 'react-redux'
import { withRouter, browserHistory } from 'react-router'
import { translate } from 'react-i18next'
import { Button, Modal, Row, Col } from 'react-bootstrap'

// have to use this global var, because setState does things at unpredictable times and dialog gets presented twice
let navConfirmed = false

@withRouter
@connect(
  state => ({ form: state.form })
)
export default class UnsavedFormModal extends Component {
  constructor(props) {
    super(props)
    this.areThereUnsavedChanges = this.areThereUnsavedChanges.bind(this)
    this.state = ({ unsavedFormDialog: false })
  }

  areThereUnsavedChanges() {
    return this.props.form && Object.values(this.props.form).length > 0 &&
      Object.values(this.props.form)
        .findIndex(frm => (Object.values(frm)
          .findIndex(field => field && field.initial && field.initial !== field.value) !== -1)) !== -1
  }

  render() {
    const moveForward = () => {
      this.setState({ unsavedFormDialog: false })
      navConfirmed = true
      browserHistory.push(this.state.nextLocation.pathname)
    }
    const onHide = () => this.setState({ unsavedFormDialog: false })

    if (this.areThereUnsavedChanges() && this.props.router && this.props.routes && this.props.routes.length > 0) {
      this.props.router.setRouteLeaveHook(this.props.routes[this.props.routes.length - 1], (nextLocation) => {
        if (navConfirmed || !this.areThereUnsavedChanges()) {
          navConfirmed = false
          return true
        } else {
          this.setState({ unsavedFormDialog: true, nextLocation: nextLocation })
          return false
        }
      })
    }

    return (
      <div>
        {this.props.children}
        <Modal show={this.state.unsavedFormDialog} onHide={onHide} bsSize="sm" aria-labelledby="contained-modal-title-md">
          <Modal.Header>
            <Modal.Title id="contained-modal-title-md">WARNING: unsaved changes</Modal.Title>
          </Modal.Header>
          <Modal.Body>
            Are you sure you want to leave the page without saving changes to the form?
            <Row>
              <Col xs={6}><Button block onClick={onHide}>Cancel</Button></Col>
              <Col xs={6}><Button block onClick={moveForward}>OK</Button></Col>
            </Row>
          </Modal.Body>
        </Modal>
      </div>
    )
  }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM