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

V4 Feature Request: Blocking navigation with custom render instead of browser alert/confirm #4635

Closed
fkrauthan opened this issue Mar 5, 2017 · 41 comments

Comments

@fkrauthan
Copy link

I looked thru the V4 website and examples and saw that you guys created a <Prompt> component. As I understand this will trigger a default Browser alert dialog. Is there anyway for certain areas to Overwrite that? My use-case would be for example to display a Bootstrap modal window instead of an unstyled Browser prompt. Or is that already possible? If yes a nice and easy to use API component (similar to the Prompt). Maybe something like <Locked> or <Blocked> where you can pass in the component you wanna display instead (or maybe just block without displaying anything?) would be amazing.

@pshrmn
Copy link
Contributor

pshrmn commented Mar 5, 2017

You are looking for getUserConfirmation

@pshrmn pshrmn closed this as completed Mar 5, 2017
@fkrauthan
Copy link
Author

Well that is not really what I am looking for. I could hardcoded that it always will be denied but not sure if that really is something that makes sense. I would rather have a component that could render a react component within the same scope as user confirmation.

@pshrmn
Copy link
Contributor

pshrmn commented Mar 6, 2017

Within your getUserConfirmation function you would render your modal component. That component would probably have some buttons and they would call the callback function to confirm/reject navigation.

@fkrauthan
Copy link
Author

But how would I render a react component in that method? And how could I change the react component for different Page components? That method is on the global routing object.

@phpnode
Copy link

phpnode commented Mar 6, 2017

@fkrauthan your question isn't related to react router, your question is really "how do i show a react modal when an event happens?" and the answer to that is going to depend on how you've structured the rest of your app.

@fkrauthan
Copy link
Author

@phpnode No my question is very much related to react router: How can I prevent react router from switching routes and instead get a callback in my current component where I embed the navigation block. I think the current system and so far proposed addition are not really the react way and are very inflexibel as they mainly relay on outside react libraries for displaying modals and/or browser alert/confirm dialogs.

@aaronschwartz
Copy link

aaronschwartz commented Mar 16, 2017

I am also running into the same limitations with getUserConfirmation.
I'm unable to accomplish what I was able to do with v3. Is there a better way to accomplish what I'm trying to do?

My use case
I have subpages that have data that can be changed and saved.
If the user changes any fields on the page, and they click the browsers Back button, I show a custom warning dialog asking them if they are sure they want to leave.
If there were no changes done on the page, there is no warning dialog.

My current solution in v3
I dynamically add the route leave hook which I have access to through props.route.setRouteLeaveHook() whenever a user changes a field on the page. I do this on individual components which know whether or not they have unsaved changes on the page.

My concern in v4
I can't find a way to dynamically change the getUserConfirmation function from arbitrary components.

This is a dealbreaker for me upgrading right now unless I can find a solution that behaves in a similar fashion. Right now in v3 it feels like a very clean way to handle this use case. Any solutions would be appreciated.

EDIT
Just noticed the Prompt component. Maybe that's what I was missing.

@pshrmn
Copy link
Contributor

pshrmn commented Mar 16, 2017

@tacticalcoding getUserConfirmation just defines how to prompt the user for confirmation (the default way being to open a "confirm" window). What you need to use is a <Prompt>.

@fkrauthan
Copy link
Author

Well again the Prompt Component does show you an alert or what ever you return as a dialog for getting user confirmation. It does not allow you to display a custom React component instead as soon as someone clicks on a link.

@aaronschwartz
Copy link

aaronschwartz commented Mar 16, 2017

I think I'm getting closer, and Prompt is really helpful for deciding when to show the message which I missed before.

But is there a way render a custom component in getUserConfirmation? I've tried a few variations of this but am not really sure that I can get it to work.

        <BrowserRouter getUserConfirmation={(message, callback) => {
            render(<UnsavedChangesWarning onAnswer={callback}/>, document.getElementById('unsavedchanges'));
        }}>

This does work to show it, but I'm not sure how to unmount the component to close it. I'm currently experimenting with some other methods.

Whats the proper way to render a component like you mentioned in a previous comment in the getUserConfirmation function?

Thank you for your help. I do appreciate the thoughtfulness that goes into this library.

@pshrmn
Copy link
Contributor

pshrmn commented Mar 16, 2017

@tacticalcoding Here is a very rough working implementation of getUserConfirmation http://codepen.io/pshrmn/pen/MpOpEY

@fkrauthan
Copy link
Author

@pshrmn But this has the disadvantage that you do not have access to your current route information as well as access to e.g. redux. A solution like this would cause a lot overhead and would not allow to change the type of Popup per use-case/page.

@aaronschwartz
Copy link

@pshrmn Thank you that is helpful to me. Is there any worry about calling render in a callback like that overwriting the previous value?

I see this comment taken from the docs but it is a bit vague:
https://facebook.github.io/react/blog/2015/10/01/react-render-and-top-level-api.html

This is important and often forgotten. Forgetting to call unmountComponentAtNode will cause your app to leak memory. There is no way for us to automatically detect when it is appropriate to do this work. Every system is different.

@pshrmn
Copy link
Contributor

pshrmn commented Mar 16, 2017

@tacticalcoding subsequent render calls will update the existing component. If you are really concerned, you could architect this in a way that you unmount the component after the user clicks the button. Something along these lines (haven't actually tried it myself).

const getUserConfirmation = (message, callback) => {
  const holder = document.getElementById('modal')
  const confirmAndUnmount = (answer) => {
    ReactDOM.unmountComponentAtNode(holder)
    callback(answer)
  }
  ReactDOM.render((
    <Popup message={message} confirm={confirmAndUnmount} />
  ), holder)
}

@fkrauthan If a <Prompt>'s message prop is a function it will be called and its result will be passed to getUserConfirmation. You could make it so that instead of a string message, it would return an object containing information on the type of component that the getUserConfirmation call should render. You might have to get a little creative, but I'm pretty sure that is all you need to render different confirmation components.

@fkrauthan
Copy link
Author

But would there not be an option to create a react component that renders a children when it receives a transition. Passes in a callback that as soon as the child component calls it either accepts or deny the transition. I know it might be harder to code in the history project but that would be the ultimate solution to make everyone happy (e.g. adding a advances transition prevention API) or just adding some extra callbacks that I could register for that would call my component if a transition would happen so I could set the block transition and get a callback to confirm/deny it.

@pshrmn
Copy link
Contributor

pshrmn commented Mar 17, 2017

I think that you've lost me now. Are we on the same page that the modal needs to be rendered separately from the rest of the application?

@fkrauthan
Copy link
Author

Yes and that is exactly what I don't like right now (that's why I created this ticket). I want to have the page prevention rendering embedded in my current react application, Creating a second "application" is not really an option (your solution is to create a new react application within getUserConfirmation). I just think it breaks with react conventions while everything else in V4 is amazing.

@pshrmn
Copy link
Contributor

pshrmn commented Mar 17, 2017

I view a custom getUserConfirmation as a way to add a prettier window.confirm. Within your application you generate some type of message, and then it gets passed off to something else that grabs the users attention and waits for their input before letting your application know what it should do next. During that time, your application should be in a frozen state, so have the confirmation be in another application makes sense to me.

You are obviously free to make a PR, but you would really need to champion it with examples of why your changes are necessary and why similar outcomes cannot be done with the current system. Right now, I just don't see there being a push to change how transition blocking works.

@robertgonzales
Copy link

Posted a working example here: https://gist.github.com/robertgonzales/e54699212da497740845712f3648d98c

To connect your application's state you can partially apply whatever props you want to getUserConfirmation and pass them to your UserConfirmation component.

A custom render prop on Prompt seems more intuitive but I'm not sure it's as flexible. Most of the time your prompt should render in a separate tree anyways.

@fkrauthan
Copy link
Author

Again I see big issues if you for example want a prompt do you wanna save first. And then when clicking yes I wanna be able to save the current form fields (e.g. API call) and then allow navigating away. use cases like that to me seem very common in web application and it feels like that having to render my Modal as a separate react application would make my live extreme hard. Or would your suggestion be for a use-case like that?

@robertgonzales
Copy link

Not saying there can't be improvement on the current API, but it's not hard to configure:

configureUserConfirmation(whateverYouWantToPass) {
  return function getUserConfirmation(message, callback) {
    // setup render (see https://gist.github.com/robertgonzales/e54699212da497740845712f3648d98c#file-getuserconfirmation-jsx)
    render(
      <UserConfirmation {...whateverYouWantToPass} />
    )
  }
}

// pass props, state, a callback to save your form, etc.
<Router getUserConfirmation={configureUserConfirmation(whateverYouWantToPass)}>
  {...}
</Router>

@fkrauthan
Copy link
Author

So I can have multiple Router components in my tree without losing to much performance?

@robertgonzales
Copy link

That doesn't sound like a good idea. You should be able to use just the one. Ofc your form will need to be connected to a top-level store.

@fkrauthan
Copy link
Author

Well but if I have two forms I would have to pass in different props^^ So I would need two routers. That's why I am saying the workaround is ok. But not very flexible. And just covers a small subset of actual real life features we might need for a web application.

@robertgonzales
Copy link

robertgonzales commented Mar 28, 2017

Well but if I have two forms I would have to pass in different props

Right, your form would need to set a config object in a top level store. That config could be different depending on the form. You don't need two routers, I promise. :)

@fkrauthan
Copy link
Author

Well but that would force me to manage UI Flows on two different locations. One time at the form itself. The second time I duplicate a lot logic for my alert dialog. Kinda makes the whole react approach useless if you ask me.

@amirmohsen
Copy link

Can we open this issue again? I am having the same problem as @fkrauthan. As far as I can see, there hasn't been any viable solution proposed here. As @fkrauthan has mentioned above, the "getUserConfirmation" method is far too limited and has too much of an overhead as it is treated as a separate react app. It only accepts a message argument and is isolated from the rest of the app's context.

It would be great if the Prompt component worked similar to the Route one:

<Prompt when={this.props.unsaved} component={Confirmation}/>
// and
<Prompt when={this.props.unsaved} render={() => <Confirmation/>}/>

@KevinGorjan
Copy link

+1

I'm struggling with the same problem. Always worked with RR v3 and used setRouteLeaveHook, but at the moment, we are using RR v4 and there is no clear example how to accomplish this.

rzueger added a commit to tocco/tocco-client that referenced this issue Apr 11, 2017
To show a confirmation box, only the `<Prompt>` component has to be added with
the desired message.

However, by default, the native confirmation dialog of the browser is displayed
which is not that pretty. To display a nicer toastr confirmation box, we have
to use the `getUserConfirmation` option when we create the history.

At the moment, there is an ongoing discussion about how to use a custom
component for the dialog here:
remix-run/react-router#4635
@jeremythuff
Copy link

I agree that using v4 for these sorts of things has proven very difficult. Is there a way for child components to define the handler for getUserConfirmation? And not using JSX. It would be nice if withRouter gave you access to setting getUserConfirmation. That would be ideal from my perspective.

@jeremythuff
Copy link

jeremythuff commented Apr 27, 2017

I have achieved something along these lines using this technique. In a component that is decorated withRouter:

componentDidMount() {

        this.unblock = this.props.history.block((nextLocation)=>{
            if(this.props.dirty) {
                this.setState({
                    openModal: true,
                    nextLocation: nextLocation
                });
            } 
            return !this.props.dirty;
        });

    }

    componentWillUnmount(...args) {
        this.unblock();
    }

I can then pass all the needed information on to my modal component which can allow the user to make choices concerning how the application will navigate.

@fkrauthan
Copy link
Author

@jeremythuff how do you prevent the modal from opening that is triggered by calling history.block? Do you just provide a empty callback for getUserConfirmation? It seems like a good component to be integrated into react router itself.

@danielbanfield
Copy link

+1 to jeremythuffs solution. Just wanted to leave a note on how and why it works as it was unclear to me at first.

As he says you can drop this in any component that has withRouter or is a route component, so you can get access to props.history.

The function passed to history.block is often shown returning a string in examples
https://www.npmjs.com/package/history#blocking-transitions

However you can just return a boolean to say
true: allow the transition
false: block the transition

So when he returns !this.props.dirty this will block the transition if dirty = true.

The next part of it is that nextLocation provided to the block function will contain where the user was attempting to go before it was potentially blocked.

So then, as in his code you can set the state to indicate that the modal should be displayed, and also take note of the location that the user was attempting to go to.

So the rest of the code would look something like this:

render() {
  return (<div>
      [other layout]
      <LeaveConfirm onConfirm={this.onConfirm} onCancel={this.onCancel} isOpen={this.state.openModal}/>
    </div>);
}

private onCancel = () => {
  this.setState({ nextLocation: null, openModal: false });
}

onConfirm = () => {
  this.navigateToNextLocation();
}

private navigateToNextLocation() {
  if (this.state.blockedData.action == 'PUSH') {
    this.props.history.push(this.state.nextLocation.location.pathname);
  } else {
    this.props.history.goBack();
  }
}

@dxinteractive
Copy link

Just as an FYI, here's a very relevant discussion about this exact feature, albeit not in regards to react router 4 specifically: remix-run/history#14

@fkrauthan
Copy link
Author

Well that discussion seems to be shutdown again with the remark that the library does not care for advance users and rather just do very simple use-cases that look good on hello world projects?

@scriby
Copy link

scriby commented Jun 7, 2017

It would be nice to have first class support for something similar to @jeremythuff's solution built into react-router. I think the basic concept could just be a BlockNavigation component which has a callback that informs the parent component when navigation was attempted, and it can allow the parent to choose how to proceed (and render whatever it wants).

Of course, it's also possible to implement such a component in user land, which is probably what we'll end up doing in our app.

The existing solution offered by react-router is nice for use cases where the navigation prompts are not context aware and only the message varies, but it's not so nice when the prompts need to be context aware and vary more than just the message.

@bummzack
Copy link

I stumbled across this issue, because I also found Prompt to be slightly limited. The getUserConfirmation works, but as soon as the dialog/modal is a bit more complex and requires something like i18n, it's really harmful or infeasible to open a new app context.

The solution provided by @jeremythuff is really awesome. I put it into a reusable component, using the FaCC pattern (could probably be implemented as HOC as well). Here it is: https://gist.github.com/bummzack/a586533607ece482475e0c211790dd50

@amirmohsen
Copy link

@bummzack Nice job! It would be great to turn this into a package (since React Router maintainers seem unwilling to add it to core).

@timdorr
Copy link
Member

timdorr commented Jun 29, 2017

If you want to submit a PR for another package that implements it, that's exactly why we adopted the monorepo format. We're more than happy to integrate multiple projects into this one repo. Please give us your PRs!

@amirmohsen
Copy link

@timdorr that's interesting. :) I didn't know that.

@pocketjoso
Copy link

pocketjoso commented Aug 31, 2017

Just wanted to chime in to say that after coming to this issue and first thinking this was going to be a pain to upgrade from react-router 3 to 4 (showing a custom modal before leave) it turned out to be a one line code change for us, via the answer from @jeremythuff (here):
from (react-router 3):
this.props.router.setRouteLeaveHook(this.props.route, this._routerWillLeave)
to (react-router 4):
this.unblockRouter = this.props.history.block(this._routerWillLeave)
(and calling this.unblockRouter() in unmount method.

Our _routerWillLeave method stayed exactly the same as before, receiving the nextLocation, returning true/false based on whether we want to allow the navigation or not, and handling the opening of a modal as we wanted.

@ZacharyRSmith
Copy link

ZacharyRSmith commented Oct 22, 2017

UPDATE: react-router-navigation-prompt now confirms when navigating away from site, dependent on browser implementation. (UPDATE 2: fixed bug where history.block()'s callback accessed stale props)

I adapted @bummzack 's solution to create this npm module: react-router-navigation-prompt. It shows its children only when @bummzack 's isOpen would be true or when renderWhenNotActive is passed:

Simplest example:

<NavigationPrompt when={this.state.shouldConfirmNavigation}>
  {({onConfirm, onCancel}) => (
    <ConfirmNavigationModal when={true} onCancel={onCancel} onConfirm={onConfirm}/>
  )}
</NavigationPrompt>

Complex example:

<NavigationPrompt
  beforeConfirm={this.cleanup}
  // Children will be rendered even if props.when is falsey and isActive is false:
  renderIfNotActive={true}
  // Confirm navigation if going to a path that does not start with current path:
  when={(crntLocation, nextLocation) => !nextLocation.pathname.startsWith(crntLocation.pathname)}
>
  {({isActive, onCancel, onConfirm}) => {
    if (isActive) {
      return (
        <Modal show={true}>
          <div>
            <p>Do you really want to leave?</p>
            <button onClick={onCancel}>Cancel</button>
            <button onClick={onConfirm}>Ok</button>
          </div>
        </Modal>
      );
    }
    return (
      <div>This is probably an anti-pattern but ya know...</div>
    );
  }}
</NavigationPrompt>

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