Responsive Layout
useWindowDimensions, rotation, tablets and foldables, and why a height that fits your phone clips on someone else's.
const { width, height, fontScale, scale } = useWindowDimensions()There are no media queries. A layout adapts because the component reads the current size and renders differently. useWindowDimensions from react-native is the hook for that: const { width, height, fontScale, scale } = useWindowDimensions(). It returns the size of the window your app occupies, the reader's font scale, and the device pixel ratio, and the docs state it updates all of its values automatically when the screen size or font scale changes, re-rendering the component.
Dimensions.get('window') is the longhand and reads the same numbers once. Called at module scope it is captured at startup and never changes again, which is the classic bug behind a layout that is correct until the phone rotates. Written out in full, useWindowDimensions is Dimensions.get('window') in state plus a Dimensions.addEventListener('change', handler) subscription, removed on unmount.
What varies
Rotation, split screen on Android, Stage Manager on iPad, and an unfolding foldable all change the window size while the app is running. The create-expo-app template sets "orientation": "portrait" in the app config, so landscape has to be enabled with "default" before you can even test it.
Why fixed heights break
A number like height: 620 is in density-independent pixels, so it does not shrink on a smaller phone; it overflows. Percentages only resolve against a parent that has a size of its own. Prefer flex for filling, aspectRatio for shape, minHeight over height where text is involved, and maxWidth to stop a form stretching to 1000dp on a tablet.
Example
import { StyleSheet, Text, View, useWindowDimensions } from 'react-native';
export default function ResponsiveDemo() {
// Re-runs on rotation, on split screen, and when the font scale changes.
const { width, height, fontScale } = useWindowDimensions();
const isLandscape = width > height;
const isWide = width >= 600; // 600dp is a common tablet breakpoint
return (
<View style={[styles.screen, isLandscape && styles.screenRow]}>
<View style={[styles.panel, isWide && styles.panelWide]}>
<Text style={styles.label}>
{`${Math.round(width)} x ${Math.round(height)} dp`}
</Text>
</View>
<View style={styles.panel}>
{/* minHeight scales with the reader's text size instead of clipping */}
<Text style={[styles.label, { minHeight: 20 * fontScale }]}>
{`Font scale ${fontScale.toFixed(2)}`}
</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, gap: 12, padding: 16, backgroundColor: '#ffffff' },
screenRow: { flexDirection: 'row' }, // side by side once it is wider than tall
panel: {
flex: 1,
justifyContent: 'center',
padding: 16,
borderRadius: 12,
backgroundColor: '#f2f2f7',
},
panelWide: { maxWidth: 420 }, // stop the first panel sprawling on a tablet
label: { fontSize: 15, color: '#1c1c1e' },
});When to use it
- Switch a screen from stacked to side-by-side when the device is in landscape.
- Cap the width of a reading column so it does not stretch across a tablet.
- Recompute a tile size from the current window width instead of hardcoding one.
- Give a row enough height for a reader who has enlarged system text.
More examples
The longhand: Dimensions plus a change subscription
Shows the window width, updating as the device rotates. The handler receives an object with window and screen keys, which differ on Android where the app may not own the whole screen. You would only write this out when you need the value outside a component or want to react to a change without re-rendering on every one; otherwise use the hook.
import { useEffect, useState } from 'react';
import { Dimensions, StyleSheet, Text, View } from 'react-native';
export default function DimensionsLonghand() {
// Read once for the first render...
const [width, setWidth] = useState(() => Dimensions.get('window').width);
useEffect(() => {
// ...then subscribe. useWindowDimensions() is exactly this, packaged.
const subscription = Dimensions.addEventListener('change', ({ window }) => {
setWidth(window.width);
});
return () => subscription.remove(); // never leave the listener attached
}, []);
return (
<View style={styles.screen}>
<Text style={styles.label}>{`${Math.round(width)} dp wide`}</Text>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, justifyContent: 'center', alignItems: 'center' },
label: { fontSize: 17 },
});A form that is readable on a phone and on a tablet
A form card that fills the width of a phone and sits centred at 480dp on a tablet or in landscape, with no breakpoint logic at all. The pairing of width: '100%' with maxWidth replaces most of what a media query would do, and it keeps working on sizes you have never tested.
import { ScrollView, StyleSheet, Text, View } from 'react-native';
export default function CenteredForm() {
return (
// Short, bounded content, so a ScrollView is fine here; a long list is not.
<ScrollView contentContainerStyle={styles.content}>
<View style={styles.card}>
<Text style={styles.title}>Create account</Text>
<View style={styles.field} />
<View style={styles.field} />
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
// Layout for a ScrollView's children goes on contentContainerStyle.
content: { padding: 16, alignItems: 'center' },
card: {
width: '100%',
maxWidth: 480, // the whole width on a phone, capped on a tablet
gap: 12,
padding: 16,
borderRadius: 12,
backgroundColor: '#f2f2f7',
},
title: { fontSize: 20, fontWeight: '700' },
field: { height: 44, borderRadius: 8, backgroundColor: '#ffffff' },
});
Discussion