React 122 ⚛️ React Native Navigation and Native Modules
React Native applications are rarely a single screen, and they frequently need capabilities that JavaScript cannot provide. Navigation is the framework’s answer to the first problem: a navigator manages the presentation and transition between screens. Native modules are the answer to the second: a bridge that exposes platform APIs—camera, Bluetooth, secure storage—to JavaScript. Both are essential to a production application, and both require understanding the layer that sits between the React component tree and the platform.
This chapter covers navigation and native modules as they exist in React Native’s documentation. It begins with React Navigation, the community-standard library that the official docs recommend for screen management, and its core navigator types. It then addresses the native module system—both the legacy bridge and the Turbo Native Modules that replace it under the New Architecture—and the Codegen pipeline that generates type-safe bindings from TypeScript specs. The final sections cover the architectural decision between navigation libraries and the constraints that make native module development a platform-specific task.
Key point: React Navigation is a JavaScript library that orchestrates screens. A native module is platform code that JavaScript calls. Neither is part of the React Native core; both are the ecosystem’s answer to problems the core deliberately leaves open.
Why navigation and native modules are separate concerns
The navigation library choice. React Native’s core does not include a navigator. The official documentation recommends React Navigation as the standard solution, a standalone library that provides stack, tab, and drawer navigation patterns on both Android and iOS . It is maintained by the Expo team, has the largest ecosystem of third-party integrations, and is the foundation that Expo Router builds on . The choice of navigation library is the first architectural decision in a React Native application because it determines the structure of every screen.
The native module necessity. Some platform capabilities have no JavaScript equivalent. Apple Pay, Google Pay, the device’s secure enclave, Bluetooth LE, and existing Objective-C or Java libraries all require native code. The NativeModule system exposes instances of native classes to JavaScript as objects, allowing JavaScript to invoke native methods . The alternative—rewriting the native functionality in JavaScript—is either impossible or too slow.
The architectural transition. The legacy native module system is being deprecated in favor of Turbo Native Modules under the New Architecture. Turbo Modules offer lazy initialization, type-safe bindings generated from TypeScript specs, and synchronous execution options . The migration is not optional for libraries that want to remain compatible with the New Architecture, and it is the direction that React Native’s core team is moving.
a. React Navigation and the navigator types
React Navigation is configured by creating a navigator with a set of screens. The static configuration API, introduced in version 7, defines the screens in a single object and infers the types of the navigation parameters from that definition . The createNativeStackNavigator uses the platform’s native navigation primitives—UINavigationController on iOS and Fragment on Android—so that transitions and gestures behave like a native application .
import { createStaticNavigation } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const RootStack = createNativeStackNavigator({
screens: {
Home: {
screen: HomeScreen,
options: { title: 'Welcome' },
},
Profile: {
screen: ProfileScreen,
options: { title: 'Profile' },
},
},
});
const Navigation = createStaticNavigation(RootStack);
export default function App() {
return <Navigation />;
}
Inside a screen, the useNavigation hook provides the navigation object. The navigate method moves to a screen and passes parameters, which the destination screen reads from its route.params .
import { useNavigation } from '@react-navigation/native';
function HomeScreen() {
const navigation = useNavigation();
return (
<Button
title="Go to Jane's profile"
onPress={() => navigation.navigate('Profile', { name: 'Jane' })}
/>
);
}
function ProfileScreen({ route }) {
return <Text>This is {route.params.name}'s profile</Text>;
}
React Navigation also provides packages for other navigator types: @react-navigation/bottom-tabs for tab bars, @react-navigation/drawer for side drawers, and @react-navigation/material-top-tabs for swipeable top tabs . The choice of navigator determines the UI pattern and the transition behavior. A stack navigator is the foundation for hierarchical navigation; a tab navigator is for top-level sections; a drawer is for secondary navigation.
Expo Router is the alternative for projects that use Expo and want file-based routing. It is built on top of React Navigation, so the underlying navigator behavior is the same, but the routes are defined by the file system rather than by a configuration object . A file at app/product/[id].tsx automatically responds to the URL /product/123 on both mobile and web, and the deep linking configuration is generated from the file structure . The decision between React Navigation and Expo Router is largely a decision about whether the project needs automatic web support and file-based routing, or whether the explicit configuration and flexibility of React Navigation is preferred .
b. Native modules and the Turbo Module migration
A native module is a class on the platform side that implements methods callable from JavaScript. In the legacy architecture, the module is registered through a package and accessed through the NativeModules object. In the New Architecture, the module is a Turbo Native Module, defined by a TypeScript spec that Codegen uses to generate the native bindings .
The migration begins with the spec file. The spec declares the module’s name and its methods with their parameter and return types. For a local storage module, the spec might look like this:
// specs/NativeLocalStorage.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
setItem(key: string, value: string): void;
getItem(key: string): string | null;
clear(): void;
}
export default TurboModuleRegistry.getEnforcing<Spec>('NativeLocalStorage');
The codegenConfig field in package.json tells the build system where to find the spec files and what package name to use for the generated Android code . Codegen reads the spec and produces the C++, Objective-C, and Kotlin/Java bindings that the native implementation must satisfy .
The native implementation on Android extends the generated spec class. It implements the methods using the platform’s APIs—in this case, SharedPreferences for storage—and registers itself through a package that the application adds to its getPackages method .
// android/app/src/main/java/com/nativelocalstorage/NativeLocalStorageModule.kt
class NativeLocalStorageModule(reactContext: ReactApplicationContext) :
NativeLocalStorageSpec(reactContext) {
override fun setItem(key: string, value: string) {
val prefs = reactApplicationContext
.getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
prefs.edit().putString(key, value).apply()
}
override fun getItem(key: string): String? {
val prefs = reactApplicationContext
.getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
return prefs.getString(key, null)
}
override fun clear() {
val prefs = reactApplicationContext
.getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
prefs.edit().clear().apply()
}
companion object {
const val NAME = "NativeLocalStorage"
}
}
The package that registers the module extends BaseReactPackage and returns the module instance from getModule . The application’s MainApplication adds this package to the list returned by getPackages. Once registered, the JavaScript side can call the methods through the TurboModuleRegistry reference.
The legacy and Turbo systems differ in initialization and type safety. Legacy modules are initialized eagerly, which adds to startup time. Turbo Modules are lazy, loaded only when first accessed . Legacy modules have no type checking between the JavaScript call and the native method; Turbo Modules have the Codegen-generated bindings, which fail at build time if the native implementation does not match the spec.
c. The navigation decision and the module distribution
The choice between React Navigation and Expo Router follows from the project’s requirements. React Navigation is the right choice for bare React Native projects, for applications that need highly custom navigation behavior, and for teams with existing React Navigation code that does not need the migration cost . Expo Router is the right choice for new Expo projects, for applications that need web support with URL-based routing, and for teams that prefer file-based routing and automatic deep linking .
The native module distribution model determines how the module reaches consumers. A module implemented directly in an application’s ios/ and android/ directories is private to that application. A module packaged as an npm library is autolinked by React Native’s build tooling, so consumers install it with npm install and the native code is integrated automatically . The library approach is the standard for shared modules, and the autolinking mechanism is what makes the npm distribution practical.
The New Architecture migration is the current constraint on native module development. A library that has not migrated to Turbo Modules may not work when the application enables the New Architecture. The React Native Firebase project’s migration is a documented example: it migrated its core modules to Codegen Turbo Modules, removed the legacy bridge, and updated its testing infrastructure to validate the new bindings . The migration is not a small change, and it is the reason the ecosystem is gradually moving from the legacy system to Turbo Modules.
Complete Example Session
// ============================================
// PART 1: THE REACT NAVIGATION SETUP
// ============================================
// Install the core and the native stack navigator.
// npm install @react-navigation/native @react-navigation/native-stack
// npx expo install react-native-screens react-native-safe-area-context
// ============================================
// PART 2: THE NAVIGATOR CONFIGURATION
// ============================================
// Static configuration with typed screens.
import { createStaticNavigation } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const RootStack = createNativeStackNavigator({
screens: {
Home: { screen: HomeScreen, options: { title: 'Welcome' } },
Profile: { screen: ProfileScreen, options: { title: 'Profile' } },
},
});
const Navigation = createStaticNavigation(RootStack);
// ============================================
// PART 3: THE SCREEN COMPONENTS
// ============================================
// useNavigation for imperative navigation.
// route.params for reading parameters.
import { useNavigation } from '@react-navigation/native';
function HomeScreen() {
const navigation = useNavigation();
return (
<Button
title="Go to Jane's profile"
onPress={() => navigation.navigate('Profile', { name: 'Jane' })}
/>
);
}
function ProfileScreen({ route }) {
return <Text>This is {route.params.name}'s profile</Text>;
}
// ============================================
// PART 4: THE APP ENTRY
// ============================================
// The Navigation component renders the stack.
export default function App() {
return <Navigation />;
}
// ============================================
// PART 5: THE NATIVE MODULE SPEC
// ============================================
// The TypeScript spec that Codegen reads.
// specs/NativeLocalStorage.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
setItem(key: string, value: string): void;
getItem(key: string): string | null;
clear(): void;
}
export default TurboModuleRegistry.getEnforcing<Spec>('NativeLocalStorage');
// ============================================
// PART 6: THE CODEGEN CONFIGURATION
// ============================================
// package.json declares the spec directory.
// "codegenConfig": {
// "name": "NativeLocalStorageSpec",
// "type": "modules",
// "jsSrcsDir": "specs",
// "android": { "javaPackageName": "com.nativelocalstorage" }
// }
// ============================================
// PART 7: THE ANDROID IMPLEMENTATION
// ============================================
// The native class extends the generated spec.
// class NativeLocalStorageModule(reactContext: ReactApplicationContext) :
// NativeLocalStorageSpec(reactContext) {
// override fun setItem(key: String, value: String) { ... }
// override fun getItem(key: String): String? { ... }
// override fun clear() { ... }
// }
// ============================================
// PART 8: THE PACKAGE REGISTRATION
// ============================================
// The package returns the module instance.
// class NativeLocalStoragePackage : BaseReactPackage() {
// override fun getModule(name: String, reactContext: ReactApplicationContext):
// NativeModule? = if (name == NativeLocalStorageModule.NAME)
// NativeLocalStorageModule(reactContext) else null
// }
// ============================================
// PART 9: THE JAVASCRIPT USAGE
// ============================================
// The module is accessed through the spec.
import NativeLocalStorage from './specs/NativeLocalStorage';
function useStorage() {
NativeLocalStorage.setItem('key', 'value');
const value = NativeLocalStorage.getItem('key');
return value;
}
// ============================================
// PART 10: THE FULL PICTURE
// ============================================
// Navigation is a JavaScript library.
// Native modules are platform code with generated bindings.
// Both are ecosystem solutions, not core features.
export default function Root() {
return <Navigation />;
}
The ten parts build a complete navigation stack and a Turbo Native Module from spec to usage. The pattern is consistent: navigation is configured in JavaScript, and native modules are specified in TypeScript and implemented on the platform.
Quick Reference
Navigation Libraries
| Feature | React Navigation | Expo Router |
|---|---|---|
| Approach | Config-based | File-based |
| Web support | Basic | First-class |
| Deep linking | Manual config | Automatic |
| Requires Expo | No | Yes |
| Ecosystem | Largest | Growing |
Navigator Types
| Package | Pattern |
|---|---|
@react-navigation/native-stack | Stack (hierarchical) |
@react-navigation/bottom-tabs | Tab bar |
@react-navigation/drawer | Side drawer |
@react-navigation/material-top-tabs | Swipeable tabs |
Native Module Types
| System | Status | Initialization | Type Safety |
|---|---|---|---|
Legacy NativeModules | Deprecated | Eager | None |
| Turbo Native Modules | Current | Lazy | Codegen |
Turbo Module Pipeline
| Step | Artifact |
|---|---|
| TypeScript spec | specs/NativeX.ts |
| Codegen config | codegenConfig in package.json |
| Generated bindings | C++, Obj-C, Kotlin/Java |
| Native implementation | Platform class extending spec |
| Package registration | BaseReactPackage subclass |
Best Practices
✅ Do This:
// Use React Navigation for bare React Native.
createNativeStackNavigator({ screens: { ... } }) // ✅
// Use the static configuration for type safety.
const RootStack = createNativeStackNavigator({ screens: { ... } }); // ✅
// Migrate native modules to Turbo Modules.
TurboModuleRegistry.getEnforcing<Spec>('ModuleName'); // ✅
// Use Codegen for type-safe bindings.
codegenConfig: { type: 'modules', jsSrcsDir: 'specs' } // ✅
// Register the module package in MainApplication.
getPackages() { return listOf(NativeLocalStoragePackage()) } // ✅
❌ Don’t Do This:
// Don't expect React Native to include a navigator.
// Navigation is a separate library. // ❌
// Don't use the legacy NativeModules for new modules.
NativeModules.MyModule; // deprecated, use Turbo Modules // ❌
// Don't skip the TypeScript spec for Turbo Modules.
// Without it, Codegen cannot generate bindings. // ❌
// Don't assume a native module works on both platforms.
// Android and iOS require separate implementations. // ❌
// Don't forget to rebuild after adding a native module.
// Metro alone does not compile native code. // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Navigation not found | Library not installed | npm install @react-navigation/native |
| Native module not found | Package not registered | Add to getPackages in MainApplication |
| Codegen bindings missing | codegenConfig wrong or not run | Verify the config and rebuild |
| Legacy module fails on New Architecture | Not migrated to Turbo Module | Create a spec and migrate |
| iOS and Android differ | Separate implementations required | Write platform code for both |
| Deep links not working | Linking config missing | Configure linking or use Expo Router |
Real-World Examples
1. Native Stack Navigator
createNativeStackNavigator({ screens: { Home: { screen: HomeScreen } } })
2. Navigate with Params
navigation.navigate('Profile', { name: 'Jane' })
3. Read Params
function ProfileScreen({ route }) { return <Text>{route.params.name}</Text>; }
4. Tab Navigator
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
5. Turbo Module Spec
export default TurboModuleRegistry.getEnforcing<Spec>('NativeLocalStorage');
6. Codegen Config
"codegenConfig": { "name": "Spec", "type": "modules", "jsSrcsDir": "specs" }
7. Android Module
class Module(ctx: ReactApplicationContext) : Spec(ctx) { ... }
8. Package Registration
override fun getModule(name: String, ctx: ReactApplicationContext): NativeModule?
9. Legacy Access
import { NativeModules } from 'react-native';
NativeModules.MyModule; // deprecated
10. Expo Router File
// app/product/[id].tsx → /product/123
Visual
The Navigation Layer
┌─────────────────────────────────────────────────────────┐
│ REACT COMPONENTS │
│ HomeScreen, ProfileScreen, SettingsScreen │
│ │ │
│ ▼ │
│ NAVIGATOR (React Navigation) │
│ Stack, Tabs, Drawer │
│ manages screen transitions and history │
│ │ │
│ ▼ │
│ NATIVE NAVIGATION PRIMITIVES │
│ UINavigationController (iOS) │
│ Fragment (Android) │
└─────────────────────────────────────────────────────────┘
The Turbo Module Pipeline
┌─────────────────────────────────────────────────────────┐
│ TypeScript Spec │
│ specs/NativeLocalStorage.ts │
│ │ │
│ ▼ Codegen │
│ Generated Bindings │
│ C++, Objective-C, Kotlin/Java │
│ │ │
│ ▼ Native implementation │
│ NativeLocalStorageModule.kt │
│ │ │
│ ▼ Package registration │
│ NativeLocalStoragePackage.kt │
│ │ │
│ ▼ getPackages() in MainApplication │
│ JavaScript access via TurboModuleRegistry │
└─────────────────────────────────────────────────────────┘
Legacy vs Turbo Modules
┌─────────────────────────────────────────────────────────┐
│ LEGACY │
│ NativeModules.MyModule │
│ Eager initialization │
│ No type checking │
│ Deprecated │
│ │
│ ───────────────────────────────────────────────────── │
│ │
│ TURBO │
│ TurboModuleRegistry.getEnforcing<Spec>('MyModule') │
│ Lazy initialization (loaded on first access) │
│ Codegen-generated type-safe bindings │
│ Current direction │
└─────────────────────────────────────────────────────────┘
The Navigation Decision
┌─────────────────────────────────────────────────────────┐
│ Does the project use Expo? │
│ │ │
│ NO │ YES │
│ ▼ ▼ │
│ React Navigation Need web support? │
│ (explicit config) │ │
│ YES │ NO │
│ ▼ ▼ │
│ Expo Router React Navigation │
│ (file-based) (explicit config) │
│ │
│ Custom transitions or complex drawers? │
│ → React Navigation gives more control. │
└─────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Navigation standard | React Navigation |
| Core packages | @react-navigation/native, @react-navigation/native-stack |
| Native stack | createNativeStackNavigator |
| Navigate | navigation.navigate('Screen', params) |
| Read params | route.params |
| Alternative | Expo Router (file-based, web support) |
| Legacy modules | NativeModules (deprecated) |
| Current modules | Turbo Native Modules |
| Spec | TypeScript file in specs/ |
| Codegen | codegenConfig in package.json |
| Registration | BaseReactPackage in getPackages |
Key takeaways:
- React Navigation is the community standard. The official docs recommend it for screen management, and it provides stack, tab, and drawer navigators for both platforms .
- The static configuration API provides type safety. Defining screens in a single object lets TypeScript infer the navigation parameters, reducing the boilerplate that manual type declarations require .
- Expo Router is the alternative for file-based routing. It is built on React Navigation, and its routes are defined by the file system. The trade-off is automatic web support and deep linking versus explicit configuration and flexibility .
- Native modules expose platform APIs to JavaScript. The legacy
NativeModulessystem is deprecated. The New Architecture uses Turbo Native Modules, which are defined by a TypeScript spec and generated by Codegen . - The spec is the contract. The TypeScript file declares the methods and their types. Codegen generates the bindings, and the native implementation must satisfy them. A mismatch fails at build time.
- Android and iOS require separate implementations. The spec is shared, but the native code is platform-specific. A module that works on one platform may not work on the other without the corresponding implementation.
- Package registration is required. The module is not available to JavaScript until its package is added to the application’s
getPackageslist . - The New Architecture migration is the current constraint. Libraries that have not migrated to Turbo Modules may not work when the application enables the New Architecture. The React Native Firebase migration is a documented example of the process .
Remember: Navigation and native modules are the two extensions that turn a React Native component tree into a usable application. React Navigation orchestrates the screens, and native modules reach the platform APIs that JavaScript cannot. The navigation choice is a library decision with ecosystem consequences; the native module choice is an architectural decision with platform consequences. Use React Navigation for explicit control, Expo Router for file-based routing and web support, and Turbo Native Modules for any new native integration. The legacy system still works, but it is the past, and the New Architecture is the direction.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!