Navigation, native modules and Expo

Routing with React Navigation, linking native code, and when the Expo managed workflow is the right trade-off.

The community standard is React Navigation. A native stack uses the platform's own navigation controller, so transitions, gestures and accessibility behave like a system app.

import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

const Stack = createNativeStackNavigator();
const Tabs = createBottomTabNavigator();

function NotesStack() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Notes" component={NotesScreen} />
      <Stack.Screen name="NoteDetail" component={NoteDetailScreen} options={{ title: 'Detail' }} />
    </Stack.Navigator>
  );
}

export default function App() {
  return (
    <NavigationContainer>
      <Tabs.Navigator>
        <Tabs.Screen name="Home" component={NotesStack} />
        <Tabs.Screen name="Settings" component={SettingsScreen} />
      </Tabs.Navigator>
    </NavigationContainer>
  );
}

// inside a screen
navigation.navigate('NoteDetail', { id: note.id });
  • Parameters arrive as route.params; keep them small and serialisable, and load full records from a store or an API by id.
  • Typed routes (TypeScript RootStackParamList) catch a wrong route name or a missing parameter at compile time.
  • Deep linking is configured in the container's linking config, plus an intent filter on Android and a URL scheme on iOS.

Native modules

Anything the core components do not cover — Bluetooth, secure storage, payments — needs a native module. Since React Native 0.74+ the recommended approach is a TurboModule with codegen, which gives a typed, lazily-loaded bridge.

# most packages are autolinked: install, then rebuild the native app
npm install react-native-mmkv
npx pod-install ios
npx react-native run-android

# permissions still have to be declared natively
# android/app/src/main/AndroidManifest.xml
#   <uses-permission android:name="android.permission.CAMERA" />
# ios/Notes/Info.plist
#   <key>NSCameraUsageDescription</key><string>Scan receipts</string>
⚠️
Adding a native module means rebuilding the app binary — a JavaScript reload is not enough, and a hot update service cannot ship new native code. Plan native dependencies before a release, not during one.

The Expo trade-off

AspectExpo managedCommunity CLI
Native project on diskGenerated, managed by the toolYours from the start
Adding native codeConfig plugins or a dev buildEdit the native project directly
Over-the-air updatesBuilt in with EAS UpdatePossible but you wire it yourself
CI and signingEAS Build handles both platformsYou configure Gradle, Xcode and CI
Upgrade costMostly a dependency bumpManual native upgrade steps
  • Expo Go is for prototyping only: it cannot run a custom native module, so any project that needs one moves to a development build.
  • npx expo prebuild generates the native projects so you can inspect or modify them; after that you are on a bare workflow with Expo's libraries.
  • EAS Build produces signed Android and iOS artefacts from a config file, which removes the need for a Mac in the loop.

FAQ

React Native or a fully native app?
React Native wins when the UI is conventional, the team already knows React and both platforms must ship together. Go native when you need deep platform integration, heavy graphics or the very best frame timings.
Can I use web libraries?
Only if they do not touch the DOM. Anything importing document, window or a browser-only API will fail; look for a React Native equivalent such as AsyncStorage or MMKV instead of localStorage.

Core components and styling Setup and your first screen

Last refreshed 2026-09-18.