Enabling the back button to cancel confirmation dialogs in React apps
Tags: react, javascript, frontend, router
Published: 2018-10-25 17:05:36

---
title: Enabling the back button to cancel confirmation dialogs in React apps
published: true
tags: React,JavaScript,frontend,router
canonical_url: https://joshblog.net/2018/enabling-the-back-button-to-cancel-confirmation-dialogs-in-react-apps/
---

Users expect that dialogs, action sheets, alerts, drawers, and other pop-ups will close when the back button is pressed. This is especially true on Android (where the back button is globally available in all apps), but this behavior makes sense for any apps running in a web browser too — on both mobile and desktop. Usually, when a dialog is open, the back button should act the same as if the user had chosen the “Cancel” option. The worst thing that could happen (but it’s surprisingly common, unfortunately) is that the dialog will close and the browser will navigate away from the page that opened the dialog!

In a React app, we can use the browser’s history (exposed by our routing library of choice) to ensure that pressing the back button when a dialog is open closes the dialog and keeps the app on the same screen that opened the dialog. As a bonus, if the user navigates forward again, we can restore the dialog to its previous state very easily. Let’s take a look.

[![](https://thepracticaldev.s3.amazonaws.com/i/lmv22wfpt4y84oonqybr.png)](https://codesandbox.io/s/n74l8jpv6p)

For convenience, I created [an example on CodeSandbox](https://codesandbox.io/s/n74l8jpv6p) to demonstrate the final result. In this example, I’ve added [Reach Router](https://reach.tech/router/) and [Material UI](https://material-ui.com/) as dependencies to provide the routing/navigation and some UI controls. The concept that I’m demonstrating here should work just as well with [React Router](https://reacttraining.com/react-router/web/) and other UI control libraries without too many changes.

[  
 ![Edit React dialogs with back button](https://codesandbox.io/static/img/play-codesandbox.svg)  
](https://codesandbox.io/s/n74l8jpv6p)

In our example app, we have a list of users. When you click/tap a user’s name, a dialog will pop up with some actions that can be performed. Or you can press cancel to close the dialog. Our goal is to ensure that the browser’s back button will close the dialog without navigating away from the list of all users — the same as if the cancel option were chosen with a click/tap.

We can pass the user to dialog using the browser history’s state object. We’ll push a new state onto the stack (without changing the URL), and the dialog will check the current state to know when it should open or close.

Let’s start by rendering the list of users using some components from Material UI:

``` jsx
// inside render()
<List>
  {users.map(user => {
    return (
      <ListItem
        key={user.id}
        button
        onClick={() => this.selectUser(user)}
      >
        <ListItemIcon>
          <Person />
        </ListItemIcon>
        <ListItemText primary={user.name} />
      </ListItem>
    );
  })}
</List>
```

We have an array of users that we map to `ListItem` components. When a list item is clicked/tapped, we call a function called `selectUser()` that will manipulate the browser’s history.

For reference, a user in the array looks like this:

``` jsx
{ id: 1, name: "Franklin Nelson" }
```

In `selectUser()`, we use the `navigate()` function provided to our component by Reach Router to add the user to the history stack.

``` jsx
selectUser(user) {
  let newState = { selectedUser: user };
  this.props.navigate(this.props.location.pathname, { state: newState });
};
```

Notice that we use `this.props.location.pathname` (also provided by Reach Router) to ensure that the URL does not change. We’re staying at the same location, but a new history entry is added that has a different state. This means that when we go back, we’ll stay on the same page.

Next, let’s see how our `UserActionsDialog` component is rendered.

As a first step, we should check if the current history state has a selected user. If no user is selected, we’ll simply return `null` because we don’t want to render the dialog:

``` jsx
// inside UserActionsDialog's render()
let user = undefined;
let state = this.props.location.state;
if (state && "selectedUser" in state) {
  user = state.selectedUser;
}
if(!user)
{
  return null;
}
```

If a user is selected, we should render our dialog. It might look something like this:

``` jsx
// continued from above
return (
  <Dialog open>
    <DialogTitle>{user.name}</DialogTitle>
    <List>
      <ListItem
        button
        onClick={() => alert("Option 1 selected for " + user.name)}>
        <ListItemText primary="Option 1" />
      </ListItem>
      <ListItem
        button
        onClick={() => alert("Option 2 selected for " + user.name)}>
        <ListItemText primary="Option 2" />
      </ListItem>
      <ListItem
        button
        onClick={this.cancelItem_onClick}>
        <ListItemText primary="Cancel" />
      </ListItem>
    </List>
  </Dialog>
);
```

Notice that we have access to the full user object selected in the parent component. We’re accessing its `name` field here.

For simplicity, the first two actions simply call `alert()`. The third option is named “Cancel” and it should close the dialog. Clicking this item calls `cancelItem_onClick()`, which appears below:

``` jsx
cancelItem_onClick = () => {
  window.history.back();
};
```

When we call `window.history.back()`, the selected user is removed from the history stack and our `UserActionsDialog` component will re-render and return `null`. That will cause the dialog to be hidden.

Notice that we don’t do anything to cancel except to navigate back. With that in mind, we don’t need to do to do anything more to enable the back button. It just works! In fact, after we navigate back, the browser’s forward button will work too, and the dialog will re-open… including the same selected user.

Back in the parent component that contains our `List` of users, let’s see how to add our `UserActionsDialog`:

``` jsx
// this goes after the list of all users
<Router>
  <UserActionsDialog path={this.props.location.pathname} />
</Router>
```

Notice that we place `UserActionsDialog` inside a `Router` component. This ensures that the `location` prop is passed into the dialog so that we can find the selected user. You can nest routers, so it’s fine if there’s another router further up the component tree.

As I mentioned before, the route’s `path` is the same as its parent. We shouldn’t need to change the URL when selecting a user because that wouldn’t make much sense if the user wanted to share the page that contains the dialog. Since the current state object stores the user, that’s what we use to determine if the dialog should be open or closed.

Even when using some kind of router, many web apps don’t account for all cases where the back button should behave a certain way. Opening dialogs to select an action is a common place where the user might press the back button to cancel, and you don’t want your app to navigate to a different page unexpectedly. On Android, this is especially important because the global back button is such a core part of the user experience. However, it’s just as useful in a web browser on any platform, including on desktop.