skip to content

Android-Only APIs

Some APIs exist only on Android: BackHandler for hardware and predictive back, ToastAndroid, elevation and android_ripple. Interviewers ask how back handling breaks when a listener is never removed.

part ofReact Nativeoverview, primer and where to startread it →
on this pageshow

explore

questions

6

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
open as a page

In React Native, what does the Android-only elevation style do, and why does a card styled only with iOS shadow props look flat on Android?

level: middleimportance: must knowfreq 55%

basics

~20 s

elevation sets Android's native view elevation, which draws a system shadow and lifts the view above non-elevated siblings. shadowOffset, shadowOpacity and shadowRadius are iOS-only, so on Android only elevation (tinted by shadowColor on API 28+) or boxShadow produces a shadow.

open as a page

In React Native, what does ToastAndroid.show do, and what happens when the same call runs on an iOS device?

level: juniorimportance: should knowfreq 30%

basics

~20 s

ToastAndroid.show(message, duration) shows a native Android toast for ToastAndroid.SHORT or ToastAndroid.LONG. On iOS the module is a fallback stub: the call logs a 'not supported on this platform' warning and shows nothing, so iOS needs another feedback path.

open as a page

In a React Native Pressable, how does the android_ripple prop work, and why might a configured ripple never appear?

level: middleimportance: should knowfreq 28%

basics

~20 s

android_ripple draws Android's native ripple on a Pressable, configured by color, borderless, radius, foreground and alpha. It is enabled only when color, borderless or radius is set, iOS ignores it, and a covering child hides it unless foreground is true.

open as a page

After a React Native upgrade to 0.81 or later targets Android 16, what changes for edge-to-edge drawing and predictive back, and what do you verify?

level: seniorimportance: should knowfreq 33%

basics

~20 s

React Native 0.81 targets Android 16, which forces edge-to-edge drawing and turns on predictive back. Content now draws behind the system bars unless insets are applied, and BackHandler keeps working, but native onBackPressed() overrides may need migrating.

open as a page

In a React Native multi-step insurance-quote screen, why can a BackHandler listener that is never removed trap the user at step one or leave the Android back button dead after the flow closes?

level: seniorimportance: should knowfreq 40%

basics

~20 s

BackHandler keeps handlers in one global list until remove() is called. An effect that subscribes without cleanup leaves stale handlers; one that captured an old step returns true and swallows the press — at step one, or everywhere once the flow unmounts.

open as a page