Router

@ng-native/router is not a navigation library of its own. It is @angular/router - the same Router, the same route config, the same guards, resolvers and deep links - pointed at react-native-screens instead of the DOM. Reach for it as soon as an app has more than one screen: its outlets create and destroy real RNSScreen views, so a push is a genuine native stack transition with the platform's own animation, swipe-back gesture, and a real UINavigationController/Fragment underneath it, not something drawn to look like one.

Setting it up

npm install @ng-native/router @angular/router
npx expo install react-native-screens

react-native-screens is the native side: every stack, screen, header and tab bar here is one of its views. Expo Go bundles it, but a development or release build only links a native module the app lists in its own package.json, so without it those builds render Unimplemented component where the navigation should be.

import { AppRegistry } from 'react-native';
import { withComponentInputBinding } from '@angular/router';
import { mount } from '@ng-native/platform';
import { getFabricUIManager } from '@ng-native/fabric';
import { provideNativeRouter } from '@ng-native/router';
import { routes } from './app/app.routes.ts';
import { App } from './app/app.ts';

AppRegistry.registerRunnable('main', ({ rootTag }) => {
  mount(Number(rootTag), App, getFabricUIManager(), {
    providers: [provideNativeRouter(routes, withComponentInputBinding())],
  });
});

provideNativeRouter(routes, ...features) is provideRouter - it takes the same routes and the same router features, and calls it - plus what native needs on top: a PlatformLocation backed by an in-memory history instead of the browser's, a RouteReuseStrategy that detaches a screen instead of destroying it (what keeps a pushed-away screen's scroll position and text input alive), and NativeNavigation. Android's hardware back button and deep links come from @ng-native/device's HardwareBack and DeepLinks and need no wiring here - both already fall back to doing nothing off a device.

The native options are passed the same way: withLinkParent for deep links, and withHeaderDefaults and withTabDefaults for how every header and tab bar looks when a screen does not say.

withComponentInputBinding() is opt-in here exactly as it is on the web: with it, a :id param, a query param or resolved data arrives as the page's id input; without it, both outlets leave a page's inputs alone, as Angular's RouterOutlet does, and the page reads ActivatedRoute instead. The feature's options are not read, though: query params always bind, and an input with no matching key is set to undefined, which are its defaults.

The shell

A route config is an ordinary Routes array - loadComponent, children, guards, all of it. What differs is the shell: instead of a plain <router-outlet>, the root component renders a <native-stack-outlet>, and its host element becomes the stack:

import { Component } from '@angular/core';
import { SafeAreaProvider } from '@ng-native/components';
import { NativeStackOutlet } from '@ng-native/router';

@Component({
  selector: 'app-root',
  imports: [NativeStackOutlet, SafeAreaProvider],
  template: `
    <safe-area-provider>
      <native-stack-outlet />
    </safe-area-provider>
  `,
  styles: `
    :host {
      flex: 1;
    }
  `,
})
export class App {}

Every route's component is created directly inside its own screen, so screens below the top of the stack stay mounted rather than torn down - which is what makes their scroll offset, cursor position and keyboard focus survive a push. A screen popped by a swipe-back gesture or the Android back button is reported to the router as a real navigation, so the URL and the visible stack never disagree.

Anything a route path can express - routerLink, router.navigate(), guards, resolvers - keeps working exactly as it does on the web, because it is still the same Router. For a plain push, nativeRouterLink is the native equivalent of routerLink:

<pressable [nativeRouterLink]="['/detail', item.id]"><text>Open</text></pressable>

From here, Screens and navigation covers everything a URL alone cannot express - replacing a screen, presenting a modal or sheet, resetting the stack - through NativeNavigation. The native header covers <native-header> and the slots <native-header-item> places content in. Tabs covers <native-tabs-outlet> and declaring a bar as content rather than as config.

Angular components as native iOS and Android views, built and shipped with Expo.

An alpha. MIT licensed. Sponsor its development.

An independent project, not affiliated with or endorsed by Google, the Angular team or Expo. Angular is a trademark of Google LLC.