1. Platform.OS & Platform.select()
Platform.OS is a string — 'ios', 'android',
or 'web' if you're also targeting web with the same codebase — read
at runtime to branch behavior. For anything beyond a single value,
Platform.select() is the cleaner tool: it takes an object keyed by
platform and returns the matching value directly.
import { Platform } from 'react-native';
// Fine for a single value
const iconName = Platform.OS === 'ios' ? 'ios-share' : 'md-share';
// Cleaner for style objects with several platform-specific properties
const styles = StyleSheet.create({
card: {
...Platform.select({
ios: { shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 8 },
android: { elevation: 4 },
default: {}, // web, or any platform not explicitly listed
}),
borderRadius: 12,
backgroundColor: '#fff',
},
});
That shadow example is a real, common necessity: iOS renders shadows with
shadowColor/shadowOpacity/shadowRadius,
while Android ignores those entirely and uses elevation instead —
there's no single cross-platform "drop shadow" property, so
Platform.select() is doing real work here, not just tidying syntax.
2. Platform-Specific Files
When a component's implementation diverges by more than a style value — different structure, different libraries, genuinely different behavior — Metro (React Native's bundler) supports splitting it into separate files by platform extension, and picks the right one automatically at build time.
components/
ShareButton.ios.tsx
ShareButton.android.tsx
import ShareButton from './components/ShareButton';
// Metro resolves this to ShareButton.ios.tsx on iOS,
// ShareButton.android.tsx on Android -- automatically
Reach for this only when Platform.select() genuinely isn't enough —
two small style differences don't justify two separate files, but a share sheet
that uses a completely different native API on each platform usually does.
3. Safe Areas & Notches
A screen's usable area isn't the same as its physical bounds — an iPhone's notch
or Dynamic Island, and Android's status bar and gesture navigation area, all carve
out space that content shouldn't render under. react-native-safe-area-context
is the standard library for handling this correctly.
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
export default function App() {
return (
<SafeAreaProvider>
<SafeAreaView style={{ flex: 1 }}>
{/* your app */}
</SafeAreaView>
</SafeAreaProvider>
);
}
SafeAreaView handles the common case automatically — but for a custom
header that needs to sit flush with the status bar while its content stays below
the notch, useSafeAreaInsets() gives you the raw inset values to apply
exactly where you need them:
import { useSafeAreaInsets } from 'react-native-safe-area-context';
function Header({ title }: { title: string }) {
const insets = useSafeAreaInsets();
return (
<View style={{ paddingTop: insets.top, backgroundColor: '#7c5cff' }}>
<Text style={{ fontSize: 18, fontWeight: '700', padding: 16, color: '#fff' }}>
{title}
</Text>
</View>
);
}
4. Responsive Dimensions
Dimensions.get('window') returns the screen size once, at the moment
you call it — it does not update if the device rotates or a
foldable unfolds. The useWindowDimensions() hook is the reactive
alternative, and should be the default choice for anything responsive.
import { useWindowDimensions, View } from 'react-native';
function ProductGrid({ products }: { products: Product[] }) {
const { width } = useWindowDimensions();
const columns = width >= 600 ? 3 : width >= 400 ? 2 : 1;
return (
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
{products.map((p) => (
<View key={p.id} style={{ width: `${100 / columns}%`, padding: 8 }}>
{/* card content */}
</View>
))}
</View>
);
}
Because useWindowDimensions() re-renders the component on every
dimension change, this grid automatically reflows from 1 to 2 to 3 columns as the
window gets wider — a phone in portrait, that same phone rotated to landscape, and
a tablet all get the right column count with zero extra code.
5. Tablets & Foldables
Two things make tablets and foldables genuinely different from "a bigger phone": width breakpoints deserve real layout changes, not just bigger fonts, and a foldable's window can resize while the app is running — unfolding the device mid-session, not just at launch.
Dimensions.get() isn't enough
A foldable device can go from phone-width to tablet-width in the middle of a session as the user unfolds it. Code that reads dimensions once at mount and never again will render a stale, wrong layout after that happens — useWindowDimensions()'s reactivity isn't a nice-to-have here, it's the difference between a layout that keeps working and one that visibly breaks.
A common, pragmatic pattern: define two or three named breakpoints (compact, medium, expanded) and switch entire layout structures — not just column counts — at each one. A detail screen might be a full-screen push on a phone but a side-by-side master-detail pane once the window crosses the tablet breakpoint.
6. Hands-on Exercise
Build a responsive, safe-area-aware settings screen
Apply platform branching, safe area handling, and reactive breakpoints to one real screen.
Requirements:
- Build a custom header (not the default navigation header) that uses
useSafeAreaInsets()to pad correctly under the status bar/notch on both platforms. - Give the header a platform-specific shadow using
Platform.select()—shadowColor/shadowOpacity/shadowRadiuson iOS,elevationon Android. - Build a settings list below the header using
useWindowDimensions(), rendering as a single column under 500px wide and a two-column grid at 500px or wider. - Test by resizing the simulator window (or rotating a physical device) and confirm the layout reflows live, without a reload.
- Add one component split into
.ios.tsx/.android.tsxfiles — even something as simple as a platform-appropriate "Open Settings" button using each OS's real settings-deep-link approach.
If your grid doesn't reflow when you resize the simulator, double-check you're reading width from useWindowDimensions() and not a value captured once from Dimensions.get('window') outside the component — the latter won't trigger a re-render on resize.
7. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why does a card's drop shadow need Platform.select() instead of one shared set of style properties?
Why does a card's drop shadow need Platform.select() instead of one shared set of style properties?
iOS and Android use entirely different properties to render a shadow — iOS reads shadowColor/shadowOpacity/shadowRadius and ignores elevation, while Android does the reverse. There's no single cross-platform shadow property, so Platform.select() is providing genuinely different values per platform, not just tidying up syntax.
Q2
When is splitting a component into .ios.tsx/.android.tsx files the better choice over Platform.select()?
When is splitting a component into .ios.tsx/.android.tsx files the better choice over Platform.select()?
When the divergence is structural — different component trees, different libraries, genuinely different behavior — not just a handful of differing style values. A couple of style differences fit cleanly inside Platform.select(); a share sheet built on a completely different native API per platform usually justifies two separate files instead.
Q3
What problem does useSafeAreaInsets() solve that a plain View with fixed padding doesn't?
What problem does useSafeAreaInsets() solve that a plain View with fixed padding doesn't?
Notch size, Dynamic Island, and status bar height vary across devices and platforms — a fixed padding value that looks right on one device will render content under a notch, or leave too much empty space, on another. useSafeAreaInsets() returns the actual inset values for the current device, so padding adapts correctly everywhere instead of being hardcoded to one screen.
Q4
Why is useWindowDimensions() preferred over a one-time call to Dimensions.get('window') for a responsive layout?
Why is useWindowDimensions() preferred over a one-time call to Dimensions.get('window') for a responsive layout?
Dimensions.get('window') reads the size once and never updates, so a layout built from it goes stale the moment the device rotates or a foldable unfolds mid-session. useWindowDimensions() is a hook that re-renders the component whenever the window size actually changes, keeping breakpoint-based layouts correct through rotation and foldable resizing.