skip to content

In React Native, how does BackHandler decide whether an Android back press is handled by your code or exits the app?

level: middleimportance: must knowfreq 62%

answer

  1. one shared list of listeners
  2. newest listener runs first
  3. return true stops the walk
  4. nobody returns true: exitApp
  5. open Modal: onRequestClose instead

basics

~20 s

BackHandler calls hardwareBackPress listeners newest-first; the first one that returns true consumes the press and older listeners never run. If none returns true, React Native falls back to Android's default back behaviour, which leaves the app.

solid answer

~40 s

`BackHandler.addEventListener('hardwareBackPress', handler)` adds the handler to one module-level list and returns a subscription whose `remove()` takes it off again. On a press, React Native walks that list from the most recently registered handler backwards: the first handler that returns `true` consumes the event and the walk stops. If every handler returns `false` or nothing, it calls `BackHandler.exitApp()`, which hands the press to Android's default back handling and normally leaves the app. Two edges matter: while a `Modal` is open BackHandler emits nothing, because the press goes to the Modal's `onRequestClose`; and on iOS the module is a no-op stub, so the import needs no platform guard. `removeEventListener` was removed in 0.77, so `remove()` on the subscription is the only way to unsubscribe.

code

tsx · 23 lines
tsx
import { useEffect, useState } from 'react';
import { BackHandler, Text, View } from 'react-native';

export function FilterPanelScreen() {
  const [panelOpen, setPanelOpen] = useState(true);

  useEffect(() => {
    const subscription = BackHandler.addEventListener('hardwareBackPress', () => {
      if (panelOpen) {
        setPanelOpen(false);
        return true; // consumed: older listeners and the default exit never run
      }
      return false; // pass the press on
    });
    return () => subscription.remove();
  }, [panelOpen]);

  return (
    <View>
      <Text>{panelOpen ? 'Panel open' : 'Panel closed'}</Text>
    </View>
  );
}

go deeper

for a junior

Recall the contract: register with addEventListener, return true to keep the app open, call remove() on the subscription when the component goes away.

for a middle

Explain the dispatch walk: one shared list, newest handler first, the first true stops it, and nobody-true falls through to exitApp and Android's default back.

for a senior

Show how the order interacts with a navigator's own listener and with Modal's onRequestClose, and why a missing remove() turns into a dead back button elsewhere.

for a principal

Frame back behaviour as an app-wide contract: which layer owns the back press, how screens opt in, and how predictive back on Android 16 constrains custom native overrides.

## What BackHandler is **`BackHandler`** is React Native's Android-only module for the system **back action** — the hardware or gesture back that every Android user expects to "go back one level". It is exported from `react-native` on every platform, but only the Android implementation does anything: on iOS both `addEventListener` and `exitApp` are empty functions and the returned subscription's `remove()` is a no-op. That makes it safe to call from shared code without a `Platform.OS` check. The API surface in React Native 0.87 is deliberately small: - `BackHandler.addEventListener('hardwareBackPress', handler)` — registers a handler and returns a subscription object. - `subscription.remove()` — unregisters that handler. - `BackHandler.exitApp()` — asks Android to run its default back behaviour. There is no `removeEventListener`: it was removed in React Native 0.77, so code that still calls it fails and must switch to the subscription. ## How a press is dispatched All handlers from every screen and component live in **one module-level list**. When Android reports a back press, React Native does the following: 1. Wraps the native event in an event object (since 0.86 the handler receives it and can read its `timeStamp`; handlers that ignore the argument are unaffected). 2. Walks the list **from the last registered handler to the first** — the newest listener gets the first chance. 3. Stops at the first handler that returns `true`. Earlier handlers are **not called at all** for that press. 4. If no handler returns `true`, or none is registered, calls `BackHandler.exitApp()`, which invokes Android's default back handling and normally takes the user out of the app. This "newest wins" order is why registration timing matters: whichever handler subscribed most recently shadows every earlier one — including a navigation library's own listener — for as long as it keeps returning `true`. ## What the return value means | Handler returns | Effect on this press | | --- | --- | | `true` | Press consumed; no older handler runs; the app stays open | | `false` | Press passed on to the next older handler | | `undefined` / `null` | Treated like `false` — passed on | | (no handler at all) | `exitApp()` runs the default back behaviour | The most common junior bug is forgetting the `return true` after doing the custom work: the handler closes a panel **and** the press keeps travelling, so a navigator pops the screen or the app exits in the same press. ## Subscribing in a function component In a function component the subscription belongs in an effect with a cleanup, so the handler exists exactly as long as the component does: ```tsx import { useEffect } from 'react'; import { BackHandler } from 'react-native'; function useConsumeBack(shouldConsume: boolean, onBack: () => void) { useEffect(() => { const subscription = BackHandler.addEventListener('hardwareBackPress', () => { if (!shouldConsume) return false; onBack(); return true; }); return () => subscription.remove(); }, [shouldConsume, onBack]); } ``` Registration is **deduplicated by function identity**: adding the very same function twice keeps one entry, and `remove()` deletes it by identity. Inline arrow functions are new identities on every effect run, which is why the cleanup, not deduplication, is what keeps the list short. ## Edges worth knowing - **Modal.** While a `Modal` is visible, BackHandler emits no events. The back press is delivered to the Modal's `onRequestClose`, which is required on Android for exactly this reason. - **iOS.** Nothing happens and nothing throws. iOS has no system back button; swipe-back belongs to the navigator. - **`exitApp()` is not a kill switch.** It delegates to Android's default back handling, the same path an unhandled press takes; it is not a way to terminate the process. - **Navigation libraries.** React Navigation registers its own `hardwareBackPress` listener to pop screens. How screen-level listeners should cooperate with it (focus-aware subscriptions) is a navigation topic rather than a BackHandler one. - **Predictive back.** Since 0.81, apps targeting Android 16 get predictive back by default; BackHandler keeps working because React Native's activity forwards the system callback to JavaScript. ## Summary for the interview Say the three rules in order — newest first, `true` stops the chain, nobody-true means the default back (exit) — then the lifecycle rule: every `addEventListener` needs a matching `remove()`. Mentioning the Modal exception and the removed `removeEventListener` shows you have used it on a current version rather than read an old tutorial.

  • In React Native, what happens if two components each register a hardwareBackPress handler and both return true?
    Only the most recently registered one runs. The walk goes newest to oldest and stops at the first `true`, so the older handler is never called for that press. If the newer one is removed later, the older one becomes first in line again.
  • Why does a BackHandler listener not fire while a React Native Modal is visible on Android?
    The Modal is a separate native dialog that receives the back press itself and reports it through its `onRequestClose` prop, which is required on Android. BackHandler emits nothing while the Modal is open, so back logic for a modal belongs in `onRequestClose`.
  • Does BackHandler.exitApp() terminate the React Native process?
    No. It invokes Android's default back handling, the same path an unhandled back press takes, which normally closes the activity and returns the user to the previous app or home screen. It is not a process kill.

A stack of sticky notes on the back button: the newest note is read first, and if it says 'handled', nobody reads the older notes underneath; if every note says 'not mine', the button does its factory job and leaves the app.

saying these in an interview costs you the question

  • Handlers run in registration order, oldest first
  • Returning false stops other handlers from running
  • Unsubscribe with BackHandler.removeEventListener
  • BackHandler must be wrapped in Platform.OS checks or it crashes on iOS
  • BackHandler still fires while a Modal is open
  • exitApp force-kills the JavaScript process