skip to content

In React Native, what happens when the Android back button is pressed while a Modal is open, and why does onRequestClose matter?

level: middleimportance: must knowfreq 52%

answer

  1. native swallows the key
  2. JS decides via one callback
  3. visible is the only switch
  4. BackHandler goes quiet
  5. iOS sheets: swipe calls it too

basics

~20 s

Android's back press never closes a React Native Modal by itself: the modal's native dialog intercepts it and calls onRequestClose, whose handler must set visible to false. Without the handler, back does nothing, and BackHandler listeners do not fire while the modal is open.

solid answer

~40 s

A `Modal` is controlled only by `visible`. On Android the modal lives in its own native dialog, which intercepts back (and Escape) and, instead of dismissing itself, sends the press to `onRequestClose`. Your handler decides, and normally sets `visible` to `false`. The docs mark `onRequestClose` as required on Android even though the type makes it optional: without it, back does nothing and users can get stuck. While the modal is open, `BackHandler` events are not emitted, so a screen-level back handler will not run. On iOS, `onRequestClose` also fires when the user swipes a `pageSheet` or `formSheet` down. By default the sheet stays up and JavaScript decides; with `allowSwipeDismissal` the sheet closes first, and you must then set `visible` to `false` to stay in sync.

code

tsx · 30 lines
tsx
import {Modal, Pressable, Text, View} from 'react-native';

type ConfirmDeleteProps = {
  visible: boolean;
  photoName: string;
  onCancel: () => void;
  onConfirm: () => void;
};

export function ConfirmDelete({visible, photoName, onCancel, onConfirm}: ConfirmDeleteProps) {
  return (
    <Modal
      visible={visible}
      transparent
      animationType="fade"
      onRequestClose={onCancel}>
      <View style={{flex: 1, justifyContent: 'center', padding: 24, backgroundColor: 'rgba(0,0,0,0.4)'}}>
        <View style={{backgroundColor: 'white', padding: 16, gap: 12}}>
          <Text>Delete {photoName} from your backup?</Text>
          <Pressable onPress={onCancel}>
            <Text>Cancel</Text>
          </Pressable>
          <Pressable onPress={onConfirm}>
            <Text>Delete</Text>
          </Pressable>
        </View>
      </View>
    </Modal>
  );
}

go deeper

for a junior

Recall that a Modal is shown and hidden only through visible, and that on Android the back button calls onRequestClose, where you set visible to false.

for a middle

Explain the mechanism: the native dialog intercepts back and reports it instead of closing, BackHandler goes silent while the modal is open, and on iOS sheet swipes also route through onRequestClose.

for a senior

Choose a back policy per modal, such as cancel for a confirmation or blocking for a critical upload, and keep native and React state in sync when allowSwipeDismissal lets iOS close a sheet first.

for a principal

Standardise modal dismissal across the app, deciding which overlays may block back and how swipe dismissal is treated, so users and support see consistent behaviour on both platforms.

## A Modal is controlled by one prop React Native's `Modal` (from `react-native`) is shown and hidden by its **`visible`** prop and nothing else. It is a controlled component in the strict sense: the native side never decides on its own that the modal should go away. Every way of closing it, whether a Cancel button, a hardware key or a swipe, has to end with your state setting `visible={false}`. The Android back button is where this design is most visible, and most often mishandled. ## What Android does with the back button When a `Modal` is open on Android it is hosted in its own native dialog window, and that window receives back presses before the rest of the app does. React Native's Android implementation deliberately stops the dialog from closing itself: 1. The user presses back (or Escape on a hardware keyboard). 2. The native modal host intercepts the key and **does not dismiss the dialog**. 3. It dispatches a request-close event, which reaches your **`onRequestClose`** callback. 4. Your callback decides. It usually sets `visible` to `false`, and the modal then closes. The source comment states the intent directly: the back key is captured so that JavaScript "can make the decision as to whether or not to allow the back/escape key to close the dialog". ## If onRequestClose is missing The docs label `onRequestClose` as **required on Android** (and on TV), although the TypeScript type marks it optional and nothing forces you to pass it. Without it, pressing back does **nothing visible**: the native side still swallows the key and reports it, but no handler turns the report into `visible={false}`. The user is stuck unless your UI offers another way out. That is the classic bug report: "back button doesn't close the dialog on Android." ## BackHandler while a Modal is open `BackHandler` is React Native's JavaScript API for the Android back button; how to use it in general is covered elsewhere. The part that belongs to `Modal` is this: according to the Modal docs, **`BackHandler` events are not emitted while a modal is open**. A screen-level back handler that, for example, asks "discard changes?" will not run while the modal is showing. `onRequestClose` is the only hook for back presses in that state. ## iOS: sheets and swipe-to-dismiss iOS has no back button, but `onRequestClose` still matters there: | Setup (iOS) | Swipe down on the sheet | What your code must do | |---|---|---| | `presentationStyle` `pageSheet` or `formSheet`, `allowSwipeDismissal` false (default) | sheet stays; `onRequestClose` fires | decide, and set `visible` to `false` if closing | | same, with `allowSwipeDismissal` true | sheet is dismissed natively; `onRequestClose` fires afterwards | set `visible` to `false` to match what already happened | | `fullScreen` or `overFullScreen` | no documented swipe dismissal | nothing extra | When `allowSwipeDismissal` is `true` but there is no `onRequestClose`, React Native logs a dev warning that the prop is required "to prevent state corruption": the sheet would be gone while your state still says `visible: true`. ## Choosing a back-button policy In a photo-backup app, two modals want different answers: - **Confirm-delete dialog**: back means cancel. `onRequestClose={onCancel}` hides the modal and deletes nothing. - **Blocking "Backing up..." overlay**: back should either do nothing, if the upload must finish, or cancel the upload and then hide the overlay. Passing a handler that deliberately ignores the press is a legitimate choice. Forgetting the handler, so that the policy happens by accident, is not. - **Anything with unsaved input**: ask first, since screen-level back handlers are silent while the modal is open. ## Verifying it before release Back-button behaviour is easy to miss because many developers test on an iOS simulator first. A short checklist for every modal: 1. On an Android device or emulator, open the modal and press back. Does it do what the product decided: close, cancel the operation, or deliberately nothing? 2. Press back twice quickly. The handler should be idempotent, so a second call while the modal is closing must not start a second action. 3. On iOS, if the modal uses `pageSheet` or `formSheet`, swipe it down on a larger device and confirm that the state follows the screen. 4. With a hardware keyboard attached to an Android device, press Escape. It takes the same `onRequestClose` path as back. ## Common mistakes - Expecting the modal to close on back "because it's a dialog". - Registering a `BackHandler` listener to close the modal. It will not fire while the modal is open. - Keeping two sources of truth, such as a native swipe dismissal and a stale `visible` flag.

  • How do you make a React Native Modal ignore the Android back button while an upload must finish?
    Pass an `onRequestClose` that deliberately does nothing, or that shows a hint, and keep `visible` true until the upload completes. The native dialog already swallows the back press, so ignoring it keeps the modal up. Make that choice explicit in code, and give users some visible progress so the screen does not look frozen.
  • Why does React Native warn when allowSwipeDismissal is true but onRequestClose is missing?
    With `allowSwipeDismissal`, iOS dismisses the sheet natively and only then calls `onRequestClose`. Without the handler, nothing sets `visible` back to `false`, so React state still says the modal is open while the screen shows it closed. The dev warning calls this state corruption; the next `visible` change can then behave unexpectedly.

A hotel door with a doorman: guests cannot push it open themselves. Pressing back only rings the doorman (onRequestClose), and the door opens only if he decides to open it (visible set to false).

saying these in an interview costs you the question

  • The Android back button closes a Modal automatically, like a native dialog.
  • onRequestClose is optional on Android because the modal handles back itself.
  • A BackHandler listener is the right way to close an open Modal.
  • iOS never calls onRequestClose because iOS has no back button.
  • Setting visible to false is optional once the user has swiped a sheet away.