"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.