React Native Fundamentals Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
Search across all documentation pages
A condensed summary of the 25 most important best practices drawn from every page in this section.
Wrap strings in Text: React Native has no DOM text nodes - placing a bare string inside View throws a redbox. Every user-visible character must live inside <Text>.
Know the flex default: React Native flex defaults to column, opposite of web CSS. Set flexDirection: "row" explicitly whenever you want horizontal layout.
Cache styles with StyleSheet.create: Defining style objects inline creates new references every render. StyleSheet.create registers styles once and lets the native side cache them for better performance.
Apply safe area once at the screen root: Nesting SafeAreaView components or combining them with manual paddingTop: insets.top doubles the inset. Pick one owner per axis - usually the outermost screen wrapper.
Use contentContainerStyle for ScrollView padding: Padding on ScrollView itself clips scroll indicators oddly on iOS. Put inner spacing on contentContainerStyle, not the scroll container.
Pair iOS shadows with Android elevation: shadowColor and shadowOffset are ignored on Android. Use Platform.select to apply iOS shadow props alongside elevation.
Nest Text inside Text for inline spans: Wrapping individual words in View breaks inline text flow and hurts accessibility. Use nested <Text> nodes for bold, colored, or linked inline spans.
Treat props as read-only: Mutating a prop or state object in place (todo.done = true; setTodos(todos)) may skip re-renders because the reference is unchanged. Always return new objects and arrays from updaters.
Derive computable values during render: Storing filtered or sorted lists in separate useState doubles updates and risks drift. Use useMemo to derive from the single source of truth.
Stabilize callbacks in virtualized lists: Inline onPress={() => toggle(id)} inside renderItem breaks memo every render. Pass stable useCallback handlers or let memoized row components own the handler.
Memoize context value objects: value={{ user, theme }} recreated each render re-renders every consumer. Memoize the value object or split contexts by update frequency.
Profile before over-memoizing: useMemo and useCallback carry their own cost. Optimize list rows and genuinely expensive pure computations - not every inline object.
Prefer Pressable over TouchableOpacity: Pressable exposes pressed, hovered, and focused states, ships android_ripple and hitSlop first-class, and avoids fading opacity across an entire subtree.
Hit 44pt minimum touch targets: A 20×20 icon without padding fails HIG and Material minimums. Add hitSlop or wrap with padding so the interactive area reaches roughly 44×44 points.
Show disabled state visually and semantically: disabled silences onPress but the control may still look active. Combine disabled styles, accessibilityState={{ disabled: true }}, and pointerEvents where overlays compete.
Use Platform.select for small deltas: Splitting .ios and .android files for a one-line color difference adds maintenance overhead. Reserve platform-specific files for materially different UI or native imports.
Import the base filename, not .ios: import X from "./Foo.ios" breaks Android bundles. Import ./Foo and let Metro resolve the correct platform file.
Always include a default in Platform.select: Web and other Expo targets get undefined without a default key. Every Platform.select call in a multi-platform app needs a fallback.
Set explicit dimensions on remote images: { uri } images without width, height, or aspectRatio render at 0×0 and appear invisible. Always constrain layout for network sources.
Keep require() paths static: Metro analyzes asset imports at build time - require('./assets/' + name + '.png') fails. Use a constant lookup map of static requires instead.
Preload assets before hiding the splash: Hiding the splash immediately while images decode shows a blank flash. await Asset.loadAsync(...) then call SplashScreen.hideAsync().
Subscribe to dimension changes with a hook: Dimensions.get('window') is a snapshot - UI stays in portrait layout after rotation. Use useWindowDimensions() or Dimensions.addEventListener.
Use width breakpoints for tablets cross-platform: Platform.isPad only covers iOS. A logical width breakpoint (e.g., >= 768) works on Android tablets and foldables too.
Assume New Architecture by default on RN 0.86: Expo SDK 57 ships with Fabric and TurboModules enabled. Check library New Architecture compatibility before upgrading - build success does not guarantee runtime stability.
Debug Hermes with React Native DevTools: Remote Chrome debugging does not attach faithfully to Hermes bytecode. Press j in Metro or use the dev menu to open React Native DevTools for breakpoints and profiling on device.
react-native core or Expo modules that ship with the default template.expo-asset assume an Expo or prebuild workflow, but the underlying rules hold in bare RN too.<Text>, even single words inside a View.memo and force every visible row to re-render on each parent tick.useCallback and keep row components memoized.Platform.select inside a shared file is cleaner.newArchEnabled in app.json / app.config.js (enabled by default in recent templates).npx expo-doctor and inspect native build logs for Fabric/TurboModule initialization.Dimensions.get('window') call in component render with useWindowDimensions().@3x asset at thousands of pixels wide and displaying it in a 48×48 avatar.expo-image with appropriate resize modes.Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 9, 2026