The Platform API
Platform tells you which OS the code is running on. Use it for conditional values, not conditional components (keep the tree clean):
import { Platform, StyleSheet } from 'react-native';
// Platform.OS โ 'ios' | 'android' | 'web' | 'macos' | 'windows'
const isIOS = Platform.OS === 'ios';
const isAndroid = Platform.OS === 'android';
// Platform.select โ clean way to provide per-platform values
const styles = StyleSheet.create({
container: {
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 4,
},
android: {
elevation: 4,
},
}),
backgroundColor: '#fff',
},
header: {
paddingTop: Platform.select({ ios: 44, android: 24 }),
fontSize: Platform.select({ ios: 17, android: 20 }),
},
});
// Platform.Version โ OS version (iOS: number string, Android: integer)
const iosVersion = parseInt(Platform.Version as string, 10);
if (Platform.OS === 'ios' && iosVersion >= 17) {
// iOS 17+ specific behavior
}
Platform-specific files
Append .ios.tsx or .android.tsx to completely swap out a fileโs implementation per platform. The bundler picks the right one automatically:
src/
components/
DatePicker.tsx โ fallback
DatePicker.ios.tsx โ used on iOS
DatePicker.android.tsx โ used on Android
// DatePicker.ios.tsx โ use iOS native date picker
import DateTimePicker from '@react-native-community/datetimepicker';
export function DatePicker({ value, onChange }) {
return <DateTimePicker value={value} onChange={onChange} mode="date" />;
}
// DatePicker.android.tsx โ Android bottom sheet picker
export function DatePicker({ value, onChange }) {
// showDatePicker is Android-specific
return <Pressable onPress={() => showAndroidDatePicker(value, onChange)}>
<Text>{value.toLocaleDateString()}</Text>
</Pressable>;
}
// Import โ no need to specify the platform
import { DatePicker } from './DatePicker';
Use platform files when the component structure differs significantly. For minor style differences, Platform.select is cleaner.
Keyboard behavior differences
iOS and Android handle keyboard appearance differently โ a common source of bugs:
import { KeyboardAvoidingView, Platform } from 'react-native';
// iOS: shift the whole view up (padding)
// Android: resize the window (OS handles it when windowSoftInputMode="adjustResize")
function LoginScreen() {
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}
>
<TextInput placeholder="Email" />
<TextInput placeholder="Password" secureTextEntry />
<Button title="Login" />
</KeyboardAvoidingView>
);
}
In AndroidManifest.xml, set android:windowSoftInputMode="adjustResize" so the OS resizes the window when the keyboard appears.
Safe areas โ notches and home indicators
import { useSafeAreaInsets } from 'react-native-safe-area-context';
function Header() {
const insets = useSafeAreaInsets();
return (
<View style={{ paddingTop: insets.top, paddingHorizontal: 16 }}>
<Text style={{ fontSize: 20 }}>My App</Text>
</View>
);
}
// Or use SafeAreaView
import { SafeAreaView } from 'react-native-safe-area-context';
function Screen({ children }) {
return <SafeAreaView style={{ flex: 1 }}>{children}</SafeAreaView>;
}
Native Modules โ when JS canโt do it
Native modules are the bridge between JavaScript and platform-specific code written in Swift/Obj-C (iOS) or Kotlin/Java (Android). You need them for:
- Accessing hardware APIs not exposed by RN (Bluetooth, NFC, custom camera features)
- Using existing native SDKs (payment, maps, analytics)
- Performance-critical code that must run on the native thread
Old architecture โ Native Module
// iOS โ ExampleModule.swift
@objc(ExampleModule)
class ExampleModule: NSObject {
@objc func getBatteryLevel(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
UIDevice.current.isBatteryMonitoringEnabled = true
resolve(UIDevice.current.batteryLevel)
}
@objc static func requiresMainQueueSetup() -> Bool { return false }
}
// JS side
import { NativeModules } from 'react-native';
const { ExampleModule } = NativeModules;
const level = await ExampleModule.getBatteryLevel();
New architecture โ TurboModule
TurboModules add TypeScript spec files and code generation, plus synchronous access via JSI:
// NativeExampleModule.ts โ the spec
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
getBatteryLevel(): Promise<number>;
getDeviceModel(): string; // synchronous โ possible with JSI!
}
export default TurboModuleRegistry.getEnforcing<Spec>('ExampleModule');
TurboModules are:
- Lazily loaded โ only initialized when first accessed (faster startup)
- Type-safe โ spec file generates native stubs
- Synchronous-capable โ can expose sync methods via JSI (not possible with the old bridge)
When NOT to write a native module
Before writing a native module, check:
- reactnative.directory โ community module catalog
- Expo SDK โ covers most common APIs (Camera, Location, Contacts, etc.)
- Expo Modules API โ allows writing native modules in Swift/Kotlin with much less boilerplate
Platform.OS and Platform.select handle per-platform values inline. Platform-specific files (.ios.tsx, .android.tsx) swap entire implementations โ good when structure differs significantly. KeyboardAvoidingView with behavior='padding' on iOS and 'height' on Android handles keyboard layout. useSafeAreaInsets accounts for notches and home indicators. Native modules are the JS-to-native bridge for APIs RN doesnโt expose โ old architecture uses NativeModules, new architecture uses TurboModules with a typed spec file and JSI access for synchronous calls and lazy loading.โ