Installation
Start a new app with the navigation or add it to the app you have, run it, and check the setup with doctor.
This page adds the navigation to an app: a new one made by init, or the app you have, set up by setup. At the end, the app starts with its first screen in a native stack, and doctor reports no problems.
Requirements
- 0.85 or newer; checked on 0.87.
- The New Architecture only.
- Xcode 26 or newer, CocoaPods.
- iOS 15.1 or newer, the minimum of React Native.
- JDK 17, the Android SDK the app builds with.
- minSdk 24 or higher.
- Expo apps: the config plugin is planned.
- The web is not a target.
Two ways to start
npx @apexnative/navigation init MyAppThe app you haveinstall and setupAdd the package; setup changes the native entry points and installs the pods.npx apex-navigation setupA new app
init makes a React Native app at the version the navigation is checked with, adds the package, runs setup and puts a starter in place of the template's App. Then it installs the dependencies and the pods.
npx @apexnative/navigation init MyApp
Run the app from its folder:
cd MyApp
npx react-native run-ios
npx react-native run-android
Home opens with an Open Details button. A tap pushes Details with the system transition; the swipe back or the back button returns to Home.
The starter is the app of the Quick start:
| File | What it is |
|---|---|
Navigation.ts | createNavigation with Home and Details in a stack. |
src/screens/Home.tsx | The first screen, with a button that pushes Details. |
src/screens/Details.tsx | The screen Home pushes. |
navigation-env.d.ts | Types of the commands, from screens: see Typed commands. |
init removes the template's App.tsx and its test. The screens render inside the navigation, and a test that renders one component alone doesn't start it.
The starters
--template picks what the new app starts with. Each starter is a working app to grow, not a demo to delete.
npx @apexnative/navigation init MyApp --template tabs
| Template | What the app starts with |
|---|---|
stack | The default, above: Home in a stack, with a button that pushes Details. |
tabs | Tabs of Home, Profile and Settings, each in a stack of its own; Home pushes Details. The tab icons are SF Symbols on iOS (house, person, gearshape), and on Android vector drawables of Material Symbols, written to android/app/src/main/res/drawable/. |
auth | Login first. Sign in puts the user into the app's context and switches to the tabs above, where Profile shows the user's name; Sign out in Settings switches back to Login. |
auth writes these files besides the screens:
| File | What it is |
|---|---|
Navigation.ts | The roots signedOut (Login) and signedIn (the tabs), the context { user } with its type exported as AppContext, and initialRoot, which picks the first root from the context. |
src/session.ts | signIn(name) and signOut(): each sets the context and switches the root. |
navigation-env.d.ts | With the context line, as Navigation.ts exports AppContext: see Typed commands. |
Login takes a name only. Your own sign-in goes where the comment in src/session.ts says, and the session you keep is read in willMount, so a user who signed in before starts on the tabs; until then each launch starts on Login. The whole flow, with links that wait for the sign-in: Sign in and sign out.
The options of init
The app it makes:
| Option | What it does |
|---|---|
--template name | The starter: stack (the default), tabs or auth. See The starters. |
--version v | The React Native of the app: a version or a tag. By default, the one the navigation is checked with. |
--pm name | npm, yarn or bun. By default, the one the command runs with. |
--directory path | The folder of the app; its name by default. |
--package-name id | The Android package and the iOS bundle id, such as com.example.app. |
--title text | The name the app shows. |
The steps it skips:
| Option | What it does |
|---|---|
--skip-install | Makes the app without installing dependencies and pods. |
--skip-pods | Installs the dependencies, not the pods. |
--skip-git-init | No git repository. |
The app you have
-
Add the package.
$ npm install @apexnative/navigation -
Set the app up for the navigation.
setupshows each change and asks before it writes. On a Mac it then installs the pods, when it changed a file of the iOS app or the pods don't have the navigation yet:pod install, orbundle installandbundle exec pod installwhen the app has aGemfile. On Linux and Windows it says to runpod installon a Mac.Terminalnpx apex-navigation setup -
Run the app.
Terminalnpx react-native run-iosnpx react-native run-androidYour
Appshows as before, now as the first screen of a native stack, named Home in the newNavigation.ts.
--dry-run shows the changes and writes nothing; --yes applies them without asking; --skip-pods leaves the pods for later and prints the line to run. A file changed by hand stays as it is, and setup prints what to change in it. Every option of every command: The CLI.
What setup changes, and why
In an app as the React Native template makes it, the window shows one React view, and all the navigation happens inside React. With this library the window shows the containers of the platform: a stack, tabs, modals, overlays. Every screen is a React view inside them. So the app hands its window and its startup to the navigation, and that is all the changes do.
Android files
| File | What setup does | Why |
|---|---|---|
MainApplication.kt | Extends NavigationApplication; keeps the template's loadReactNative(this) in onCreate, or adds it. | It keeps one React host for all screens, made after loadReactNative(this) loaded React Native. Native screens made by codegen are registered there, before JS opens them. |
MainActivity.kt | Extends NavigationActivity. | The activity holds the containers of the navigation instead of one React view. It gets the back press, edge-to-edge and configuration changes first. |
AndroidManifest.xml | Adds android:enableOnBackInvokedCallback="true" to <application>. | On the first screen the system plays its own animation back to the home screen as the user swipes back (predictive back). Android 16 turns it on for apps that target API 36; an app that targets less opts in here. A value the app set itself stays. See Back to the home screen. |
styles.xml | AppTheme inherits Theme.ApexNavigation. | The bars, tabs and search are Material 3 views and take their colors from the app's theme. This one is Material 3 in neutral grays, light and dark; change any color in the same file. A theme of the app's own stays as it is. See Android's theme. |
The main activity keeps the template's android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode". Without orientation, screenSize, smallestScreenSize, screenLayout or uiMode Android recreates the activity on a rotation, a resize, a fold or a light/dark switch, and every screen goes with it; doctor warns about it under Config changes.
iOS files
| File | What setup does | Why |
|---|---|---|
AppDelegate.swift | Extends ANNAppDelegate. | It creates the window, puts the root of the navigation in it and starts React Native. Deep links that come before JS is ready wait for it. |
SceneDelegate.swift | Extends ANNSceneDelegate. React Native 0.88 and newer only: the earlier templates have no scene delegate. | The template's scene delegate would start React Native a second time. ANNSceneDelegate moves the navigation's window into the app's scene and passes the scene's links to the app delegate. |
Info.plist | Sets UIViewControllerBasedStatusBarAppearance to true. | Each screen sets its own status bar from its options; with false, only the app as a whole can. |
JavaScript files
These files are the same on both platforms.
| File | What setup does | Why |
|---|---|---|
index.js | Starts the app through the navigation. | AppRegistry of the package starts createNavigation instead of mounting one root component. |
Navigation.ts | New: shows App as the first screen. | The app keeps working as it was, now as the first screen of a stack. |
App.tsx | Takes out the template's <StatusBar>. | React Native's StatusBar logs an error on iOS once the screens set the status bar. |
metro.config.js | Wraps the exported config in withApexNavigation. | Fast Refresh: an edit of a screen updates the running app in place, with its state, instead of reloading it. See Fast Refresh. |
navigation-env.d.ts | New: ties the commands to the screens of Navigation.ts. | The commands come from the package and can't see the app's screens. This declaration links them, so names and props are checked. See Typed commands. |
Fast Refresh
setup wraps your metro.config.js in withApexNavigation; in a new app init has done it. By hand:
const { withApexNavigation } = require('@apexnative/navigation/metro');
module.exports = withApexNavigation(mergeConfig(getDefaultConfig(__dirname), config));
It does two things. React Native 0.87's transformer doesn't apply React Refresh's Babel plugin (Metro no longer passes it hot), and without the plugin a component that its module doesn't export loses its state at every edit. And it makes the module that imports createNavigation accept hot updates, so an edit of the screens map or of the app's config updates the running app without a reload: the screens, the default options (when theme or options changed), the roots, the listeners and the link routes. What only the start reads isn't run again: the first root, willMount, the splash and the context. The app's module itself runs again, as a component's module does in Fast Refresh: code at its top level (an SDK's init, a listener added there) runs again too, so keep such code in a module of its own that doesn't change, or make it safe to run twice. The same holds for a module the app's module imports that isn't a component, such as a store: its edit runs it and the app's module again instead of reloading the app, so a store made at the top of that module is made anew while the screens that didn't run again keep the old one. Reload the app (R) after such an edit.
doctor warns under Fast Refresh when the Metro config doesn't use it. What to write in a screen file so that its edits update in place: Fast Refresh.
Typed commands
navigation.push('Details', { props }) comes from the package, and the package doesn't know the screens of your app. One declaration links them: setup and init write it in navigation-env.d.ts at the root of the app.
// Made by apex-navigation setup: the types of the app's screens
// for the navigation's commands. Don't edit.
declare module '@apexnative/navigation' {
interface Register {
app: typeof import('./Navigation').Navigation;
}
}
export {};
- It doesn't change with the screens. It holds no list of screens, only
typeof Navigation: a screen added toscreensis typed the moment the file is saved, with no command and no build. - The context too. When
Navigation.tsexports a type or an interface namedAppContext, the file registers it next toapp, with the linecontext: import('./Navigation').AppContext;: see Typing it. A type of another name isn't picked up. setupkeeps it up to date. It writes the file again when it no longer fits:Navigation.tsbegan to exportAppContext, orindex.jsimports the navigation from another module or under another name. A renamed module or export is an error of TypeScript in this file, not a silent loss of the types;doctorwarns about such a file under Typed commands, and--fixrunssetup.- Don't edit it, and commit it.
setupknows its own file by the first line, the comment. A file without that line is yours:setupleaves it as it is and prints what it would write. export {}matters. It makes the file a module, so the declaration adds to the types of the package instead of replacing them.- A
tsconfig.jsonwith its ownincludegets the file added to it bysetup, as one of the changes it shows. One that lists itsfilesinstead:setupprints the line to add. - No
tsconfig.json(a JavaScript app): the file isn't made. Everything works; screen names are plain strings and props aren't checked.doctorchecks the names the commands give instead: see Check everything with doctor.
This is how TypeScript links a package to the code of an app: the commands are imported from the package, so the app has to tell it about its screens once. Other libraries do the same with a file their tools generate.
Check everything with doctor
doctor checks what the navigation needs in the app and on the machine, and gives a fix for each problem. Run it after setup, after an upgrade, and whenever a build fails for no clear reason.
App ✓ React Native 0.87.1, New Architecture ✓ Navigation 1.0.0-alpha.0 ✓ Native files MainApplication.kt · MainActivity.kt · AndroidManifest.xml · styles.xml · AppDelegate.swift · Info.plist ✓ Entry point index.js ✓ Typed commands navigation-env.d.ts Navigation ✓ Screens 2 ✓ Links myapp:// Android ▲ JDK 17 installed, but JDK 18 is in use fix export JAVA_HOME=$(/usr/libexec/java_home -v 17) ✓ Android SDK SDK 37, build tools 37.0.0, NDK 27.1.12297006 ✓ minSdk 24 · Edge-to-edge Android 15 and newer (targetSdk 36) iOS ✓ Xcode 27.0 ✓ CocoaPods 1.15.2 ✓ Pods navigation 1.0.0-alpha.0 ✓ iOS version React Native's minimum Devices ✓ iOS simulator iOS 27.0 ✓ adb 1 device connected Conflicts ✓ None ────────────────────────────────────────────── 1 warning · 16 checks passed
| Group | What it checks |
|---|---|
| App | React Native and the New Architecture, the package, the files setup sets up, the entry point, React Native loaded twice, the status bar of the template, the generated code of native screens. Typed commands: navigation-env.d.ts there, pointing to the navigation, with its context line, and read by tsconfig.json. The splash screen made from its settings, with its logo inside the circle of Android's splash icon. The app icon made from its settings, and an iOS icon set with no image, which the App Store refuses. |
| Navigation | From the app's code. Screens: no name given twice in screens and the groups, and no file of the screens' folder with a defineScreen or nativeScreen that no file imports, a screen made and never added. Links: each prefix of linking declared on both platforms, the declarations no prefix has, a main activity that isn't singleTask. Screen names, in a JavaScript app only: a name that push, replace, showModal or showInColumn gives and no screen has, with its file and line. |
| Android | JDK 17 in use, the Android SDK, minSdk, how edge-to-edge works in the app. |
| iOS | Xcode 26 or newer, CocoaPods, the pods of the navigation and whether they are out of date, the iOS version of the Podfile. |
| Devices | A simulator and adb to run the app on. |
| Conflicts | Another navigation in the app, an Expo app. |
--fix makes the fixes the CLI has, for errors and for warnings: setup, links, icons, codegen and pod install, asking before each. --json prints the result as one JSON document, for CI, tools and AI agents. The command exits with 1 when a check finds an error; warnings leave it at 0.
Next
- The first screens: the Quick start defines two screens, puts them in a stack and pushes one from the other.
- A native screen: a screen in SwiftUI, UIKit or Compose is added with
npx apex-navigation screen. More: Native screens. - Coming from another library: From React Navigation, From Expo Router and From react-native-navigation move an app screen by screen.
- Every command: The CLI lists them all, with their options, the links, the app icon and the server for AI agents.