Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

history.block unmount and remounts the component back #589

Closed
bilalfastnu opened this issue May 14, 2018 · 6 comments
Closed

history.block unmount and remounts the component back #589

bilalfastnu opened this issue May 14, 2018 · 6 comments

Comments

@bilalfastnu
Copy link

bilalfastnu commented May 14, 2018

I've written custom blocking condition using history.block, it's triggering but on confirm "cancel" it still push on history, and remounts the current current component back.

Basically, I've timer running in the component using redux, and on dialog cancel it re-mounts the component and timer restarts again.

  componentDidMount(){
    
    const {history} = this.props;

    if(history && history.listen){

      history.listen( location => {
        console.log('Listening location change', location);
      })

      this.unblockCallback = history.block( (location, action) => {
        if(this.state.showAlert){
          return "Are you sure you want to leave the site?"
        }
        
      })
    }
  }

React Router 4.2.0
React Router DOM 4.1.1

@geoffoliver
Copy link

geoffoliver commented May 30, 2018

I can confirm this behavior. In addition, the URL changes before the user makes any decision in the confirm() dialog. For example, if I call goBack() or just click the "back" button in the browser, the URL changes to wherever "back" would take me, then the confirmation dialog appears. If I click the "Cancel" option, the URL reverts to the page currently being displayed, which then re-instantiates my component.

The same behavior occurs when I use the <Prompt> component in my component, rather than calling block() on the history prop.

@pshrmn
Copy link
Contributor

pshrmn commented May 30, 2018

Please include sandboxes (https://codesandbox.io) with running demos.

@plan8studios The history package uses history.go() (that is, the browser's History API method) for its go(Back|Forward) methods. Calling them has the same behavior as the user clicking the browser's back/forward buttons. When you POP between locations, your history instance learns about the location change through a popstate event, which is not fired until after the session history has updated the location. If you cancel the navigation, history has to revert to the previous location.

@mxgit1090
Copy link

mxgit1090 commented Jun 23, 2018

What if the following code from createBroswerHistory is removed ?

In Facebook if I have the unfinished input, back behavior is like what @plan8studios said.
When I browsed pages in then create post in news feed in Facebook. As I press the back button of the browser, the alert appears but with the URL changing.

If the code will not removed, should we add some props to control the pop event like the following code to control the popstate behavior:

createBrowserHistory({
    // ...other config
    revertPopState: true, // false performs like Facebook, no history.go(delta)
})

And for local usage, the config may changed in some use cases, so add the method:

const history = createBrowserHistory();
history.config({
    revertPopState: false,
});

OR

history.block("Block string", { revertPopState: false });

@l0gicgate
Copy link

Experiencing exact same issue. Is there a fix for this?

Obviously this is caused because there is no way to prevent the initial pop state even if you instantly revert the pop state. When used with react router it will automatically reload the route.

There needs to be some sort of middleware that prevents the default event from happening and filters all the pop state events.

@l0gicgate
Copy link

l0gicgate commented Jul 26, 2018

I've created a workaround for this. It is hackish, but it works fantastic.

The fix shims the createBrowserHistory function and also requires to create a copy of createTransitionManager since it is not importable from the history package.

The shim introduces two new methods;

  • history.lock(confirm) - Pass confirm callback, if result of the confirm() function is positive, the transition will occur. When the confirm callback is called, it gets passed a navigateToNext() callback which can be called asynchronously to navigate to the requested location in case some logic needs to be applied before executing the transition. The lock function also locks up the popstate handler as well as the push and replace public API functions of the history module.
  • history.unlock()

Here is the gist with the shimmed createBrowserHistory function:
https://gist.github.com/l0gicgate/1a49b11597f03118dc7d8a21a79e8cc4

Instead of importing createBrowserHistory from the history package you'll have to import the shimmed function.

import createBrowserHistory from './createBrowserHistory';

const history = createBrowserHistory();

Here is a sample component with the new blocking/unblocking usage.

import React from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router';

class BlockingComponent extends React.Component {
  static propTypes = {
    children: PropTypes.oneOfType([
      PropTypes.array,
      PropTypes.object,
      PropTypes.string,
    ]),
    history: PropTypes.object,
    promptBeforeTransition: PropTypes.func,
  };

  static defaultProps = {
    className: null,
  };

  componentDidMount() {
    const { history, promptBeforeTransition } = this.props;

    if (promptBeforeTransition && !history.locked) {
      history.lock(promptBeforeTransition);
    }
  }

  componentWillUnmount() {
    const { history, promptBeforeTransition } = this.props;

    if (promptBeforeTransition) {
      history.unlock();
    }
  }

  render() {
    const { children } = this.props;
    return <div>{children}</div>;
  }
}

export default withRouter(BlockingComponent);

Implementation

import React from 'react';
import 

class OtherComponent extends React.Component {
  render() {
    return (
      <BlockingComponent 
        promptBeforeTransition={(navigateToNext) => {
          if (someCondition) {
            new Promise().resolve(() => navigateToNext()); // Asynchronous usage
          }
          // If we returned true, transition would occur without calling navigateToNext();
          // Returning false stops the transition from happening
          return false;
        }}
       >
         <div>...</div>
      </BlockingComponent>
    );
  }
}

@neewbee
Copy link

neewbee commented Apr 22, 2019

#690

@lock lock bot locked as resolved and limited conversation to collaborators Jun 22, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants