React Native – Platform APIs and Device Differences

July 28, 20264 min readUpdated 8/24/2026

"Write once, run anywhere" is mostly true and precisely wrong in a handful of places. This lesson is those places — the APIs that exist because you are on a phone, and the ones that behave differently depending on which phone.

Platform

const isWeb = Platform.OS === 'web';

Platform.OS is 'ios', 'android' or 'web'. Evaluated once at module load here, because it cannot change while the app is running.

Platform.select is the tidier form when you want a value per platform rather than a branch, and Platform.Version gives the OS version — useful when an API arrived in a particular release.

Platform-specific files

For anything bigger than a value, let the bundler choose. Metro resolves Thing.ios.tsx, Thing.android.tsx, Thing.web.tsx and Thing.native.tsx automatically — import './Thing' gets the right one with no configuration and no conditional import.

This is how the demo app keeps Stripe's native SDK out of the web bundle entirely. It is a much better tool than a runtime if once the difference is more than a line, because the code for the other platform is not even bundled.

AppState — the one with no web equivalent

A browser tab lives until the user closes it. A phone app does not: the moment it is backgrounded the OS may suspend its JavaScript, and it may kill the process outright to reclaim memory — without warning, and without running any pending timer.

    const subscription = AppState.addEventListener('change', (nextState) => {
      if (nextState === 'background' || nextState === 'inactive') {
        void persist();
      }
    });

    return () => subscription.remove();

That is the demo app's cart. Changes are saved on a 300ms debounce, so a customer who adds a pizza and immediately switches apps would lose it — the timer never fires. Flushing on 'background' turns a lost cart into a saved one.

The states are active, background and — iOS only — inactive, which covers the moment during a phone call or the app switcher. Handling both is the safe read.

⚠️ Remove the listener. Subscriptions are global; a component that mounts repeatedly without cleaning up leaks a listener every time. This is the closest thing React Native has to the web's visibilitychange, and it is more consequential.

The same event is where you refresh data the user has been away from, or re-check whether a permission they went to Settings to grant is now granted.

Alert

There is no window.confirm. For a destructive action you want the platform's own dialog:

      Alert.alert(title, message, [
        { text: 'Cancel', style: 'cancel' },
        {
          text: 'Delete',
          style: 'destructive',
          onPress: () => {
            void onConfirm();
          },
        },
      ]);

style: 'destructive' renders the button in red on iOS, and style: 'cancel' positions it correctly and makes it the default. Matching the OS is what makes "are you sure?" read as serious rather than as a decoration.

Note it is callback-based, not a promise — it returns immediately and the answer arrives later. And it is asymmetric: Alert.prompt, which collects text, is iOS only. Building your own input dialog is the portable option.

⚠️ Alert is a no-op on react-native-web. If you preview your app in a browser, confirmation dialogs silently do nothing — which is exactly the kind of thing that makes you doubt your code rather than the environment.

Dimensions, and the absence of media queries

There are no media queries. Responsive layout is JavaScript, and the hook is useWindowDimensions — it re-renders on rotation, where the older Dimensions.get('window') returns a snapshot that silently goes stale.

Two distinctions worth knowing: window versus screen (the latter includes system bars on Android), and PixelRatio, which converts between the density-independent pixels you write and the physical ones the display has.

Where iOS and Android actually differ

A short list of things that will catch you, all of which the earlier lessons touched:

Shadows. iOS reads shadow*, Android reads elevation. Set both.

The back button. Android has a hardware back; iOS has a swipe. A modal needs onRequestClose or back exits the app.

The keyboard. Android resizes the window; iOS does not. Hence the per-platform KeyboardAvoidingView behaviour.

TextInput vertical alignment. Android centres, iOS does not.

Ripples. Android's native press feedback is a ripple — android_ripple on Pressable — where iOS uses opacity.

Fonts. fontFamily is one of the few genuinely platform-specific style values; the monospace face is Menlo on iOS and monospace on Android.

Other things you will reach for

Linking opens a URL, a phone number or your app's settings page. Share opens the native share sheet. Clipboard, Vibration and expo-haptics do what they say — and haptics on a confirmation is one of the cheapest ways to make an app feel native rather than ported.

What is next

Native Modules and Config Plugins — what happens when JavaScript is not enough.