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

前端 未结 5 1545
生来不讨喜
生来不讨喜 2021-02-08 11:25

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

相关标签:
5条回答
  • 2021-02-08 11:42

    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
    };
    
    0 讨论(0)
  • 2021-02-08 11:48

    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>
        )
      }
    }
    
    0 讨论(0)
  • 2021-02-08 11:49

    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)
          })
        }
      })
    }
    
    0 讨论(0)
  • 2021-02-08 12:02

    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)
        }
    }
    
    0 讨论(0)
  • 2021-02-08 12:03

    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
    };
    
    0 讨论(0)
提交回复
热议问题