Skip to content

Simple, performant & type-safe cross platform navigation in React Native / React Native Web

License

Notifications You must be signed in to change notification settings

web-ridge/react-native-ridge-navigation

Repository files navigation


react-native-ridge-navigation

Simple and performant navigation on iOS, Android and the web with simple and type-safe api.

Build on top of grahammendick/navigation. Check it out if you want more control over the navigation!

Features

  • New architecture (Fabric) ready
  • Simple api
  • 100% Type safety (routes, params, bottom tabs)
  • Bundle splitting (lazy loading, smart prefetching)
  • Render as you fetch on iOS, Android & web
  • Works on Android, iOS & web!
  • Preload data on mouseDown (or more sensitive on hover, see <Link /> example)
  • Preload component on hover (on web)
  • Automatic deep-linking
  • Real-time light/dark mode
  • Out of the box scroll restoration (on web), because screens in tab are pushed on top of each other!
  • A lot of control over the web layout
  • Universal links (already works, but docs need work)
  • PWA documentation (already works, but docs need work)

Example

Demo of react-native-ridge-navigation This is an older video which used react-native-navigation so we have newer material you bottom bar in the meantime :) View video in better frame on YouTube

About us

We want developers to be able to build software faster using modern tools like GraphQL, Golang and React Native.

Give us a follow on Twitter: RichardLindhout, web_ridge

Demo

See example: reactnativeridgenavigation.com

Source

See source of example app

Register screens

You can register screens with a preload function, the params will be automatically in string format based on the url.

// NavigatorRoutes.ts
const PostScreen = registerScreen(
  '/post/:id',
  lazy(() => import('./PostScreen'),
  ({ id }) => {
    queryClient.prefetchQuery(
      queryKeyPostScreen({ id }),
      queryKeyPostScreenPromise({ id }),
      { staleTime: 3000 }
    );

    // if you return something here it can be used in the screen itself or somewhere else with
    // usePreloadResult(routes.PostScreen)
    // in this case react-query handles it based on queryKey so it's not needed but with Relay.dev it is.
    // you can put the result of the usePreloadResult in your usePreloadedQuery if you use Relay.dev

  }
);

const routes = {
  PostScreen,
  // ...
}
export default routes

Supported stacks

  • normal
  • bottomTabs

every screen can be used within every stack. You don't have to configure screens for a stack.

Installation

1. If you don't have an app yet (optional, but recommended)

Use Expo with prebuild (Expo Go is not supported since we have native libraries)

2. Install deps + library

yarn add react-native-ridge-navigation navigation-react-native

or with npm

npm install react-native-ridge-navigation navigation-react-native

Support for Material You bottom bar in Android

1. add file expo-plugin-android-material-you-bottom-bar.js to your root folder

const { withAndroidStyles } = require("@expo/config-plugins");

module.exports = function androidMaterialYouBottomBarPlugin(config) {
  return withAndroidStyles(config, async (config) => {
    const styleFile = config.modResults;
    const appTheme = styleFile.resources.style.find(
      (style) => style.$.name === 'AppTheme',
    );
    appTheme.$.parent = 'Theme.Material3.DayNight.NoActionBar';

    return config;
  });
};

2. add this to your expo config app.config.js or app.json

"plugins": [
"./expo-plugin-android-material-you-bottom-bar"
]

Usage

import {
  createNormalRoot,
  createNavigation,
  createBottomTabsRoot,
  createScreens,
  defaultTheme,
} from 'react-native-ridge-navigation';

export const NavigationRoots = {
  RootHome: 'home',
  RootAuth: 'auth',
};

// svg to png
// https://webkul.github.io/myscale/
//
// tab icon
// http://nsimage.brosteins.com/Home
export const BottomRoots = {
  Posts: {
    path: '/post',
    title: () => 'Posts',
    icon: require('./img/palette-swatch-outline/icon-20.png'),
    selectedIcon: require('./img/palette-swatch/icon-20.png'),
    child: routes.PostsScreen,
  },
  Account: {
    path: '/account',
    title: () => 'Account',
    icon: require('./img/account-circle-outline/icon-20.png'),
    selectedIcon: require('./img/account-circle/icon-20.png'),
    child: routes.AccountScreen,
  },
};
// App.tsx
import * as React from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import AsyncBoundary from './helpers/AsyncBoundary';
import {
  createBottomTabsRoot,
  createNormalRoot,
  NavigationProvider,
} from 'react-native-ridge-navigation';

import { BottomRoot, NavigationRoots, screens } from './Navigator';
import routes from './Routes';
import AsyncBoundaryScreen from './helpers/AsyncBoundaryScreen';

const navigationRoot = {
  [NavigationRoots.RootHome]: createBottomTabsRoot(
    [BottomRoot.Home, BottomRoot.Posts, BottomRoot.Account],
    {
      breakingPointWidth: 500,
      components: {
        override: HeaderWeb,
      },
    }
  ),
  [NavigationRoots.RootAuth]: createNormalRoot(routes.AuthScreen),
};

export default function App() {
  return (
    <SafeAreaProvider>
      <NavigationProvider
        screens={screens}
        SuspenseContainer={AsyncBoundaryScreen}
        navigationRoot={navigationRoot}
      />
    </SafeAreaProvider>
  );
}

See example code for the asyncboundary stuff :)

New screen

Use the <Link /> component as much as possible since it will work with ctrl+click on the web :)

<Link
  to={routes.PostScreen}
  params={{ id: `${item.id}` }}
  // linkMode="default" // optional if sensitive the preload will be called on hover instead of mouseDown
>
  {(linkProps) => (
    <Pressable  {...linkProps}> // or other touchables/buttons
      <Text>go to post</Text>
    </Pressable>
  )}
</Link>

createLinkComponent

Use the createLinkComponent component to create re-usable links without render props. E.g to create a linkable button for react-native-paper

//ButtonLink.tsx
import { Button } from 'react-native-paper';
import { createLinkComponent } from 'react-native-ridge-navigation';

const ButtonLink = createLinkComponent(Button);
export default ButtonLink;
<ButtonLink
  to={routes.PostScreen}
  params={{ id: '2' }}
  mode="contained"
  // all optional RNP props
  // all optional Link props
>
  Go further
</ButtonLink>

Alternatives (push, replace, refresh or fluent)

const { push, replace, refresh, fluent } = useNavigation();

// call this where if you can't use <Link />
push(routes.PostScreen, {
  id: `${item.id}`
});

// call this if e.g. after a create you want to go to edit screen
// but without pushing history to the url-stack or app-stack :)
replace(routes.PostScreen, {
  id: `${item.id}`
});

// call this if you want to refresh the screen with new params
refresh(routes.PostScreen, {
  id: `${item.id}`
});

// normal root, replace full history
fluent(
  fluentRootNormal(NavigationRoots.RootAuth),
  fluentScreen(Routes.HomeScreen, {}),
  fluentScreen(Routes.PostScreen, { id: '10' }),
  fluentScreen(Routes.PostScreen, { id: '20' }),
  fluentScreen(Routes.PostScreen, { id: '30' })
);

// bottom root, replace full history
fluent(
  fluentRootBottomTabs(NavigationRoots.RootHome, BottomRoot.Account),
  fluentScreen(Routes.HomeScreen, {}),
  fluentScreen(Routes.PostScreen, { id: '10' }),
  fluentScreen(Routes.PostScreen, { id: '20' }),
  fluentScreen(Routes.PostScreen, { id: '30' })
);

Switch root

Switch root can be used to switch from e.g. the auth screen to a different entrypoint of your app. E.g. check the role and switch the stacks to different roots for different user roles.

<SwitchRoot rootKey={NavigationRoots.RootHome} params={{}} />;
// or e.g.
<SwitchRoot rootKey={NavigationRoots.RootAuth} params={{}} />;

useNavigation

All available properties

const {
  pop, // go back
  switchRoot,
  preload, // call preload (done automatic on link mouseDown
  push, // calls preload + pushes screen
  replace, // calls preload + replaces screen
  refresh, // calls preload + replaces params of screen
} = useNavigation()

Control bottom tabs

  const { switchToTab, currentTab } = useBottomTabIndex();
const { updateBadge, badges } = useBottomTabBadges();
// updateBadge(BottomRoots.Projects, '10');
// switchToTab(BottomRoot.Posts);

Modal stack

If you want a nested stack e.g. in a modal, check out the example code .

import {
  NavigationNestedProvider,
  ModalBackHandler,
} from 'react-native-ridge-navigation';

<ModalBackHandler>
  {(handleBack) => (
    <Modal
      visible={modalVisible}
      style={{ backgroundColor: theme.colors.background }}
      statusBarTranslucent={true}
      presentationStyle="pageSheet"
      animationType="slide"
      onRequestClose={() => {
        if (!handleBack()) setModalVisible(false);
      }}
    >
      <NavigationNestedProvider>
        {/* you can render your children here and push to all registered screens*/}
        <View style={{ height: 250, backgroundColor: 'pink' }}>
          <Header title="Modal stack" />
          <Button onPress={onClose}>Close modal</Button>
          <Link to={routes.PostScreen} params={{ id: '2' }}>
            {(linkProps) => <Button {...linkProps}>Account</Button>}
          </Link>
        </View>
      </NavigationNestedProvider>
    </Modal>
  )}
</ModalBackHandler>

Deep linking

You have to enable url schemes etc in your app and it'll work!

If you want to deep link into a bottom tab or other screen you can use the fluent navigation in development mode and it will log the required url to go to the right screen :)

More

  // global
  createLinkComponent,
  SwitchRoot,
  BottomTabLink,
  Link
  BackLink // for now .pop() but we'll update this according to Android guidelines later on (to always go back in hierarchy)
  lazyWithPreload // only available on the web: see example app
  Redirect,
  StatusBar,
  FocusAwareStatusBar,
  NavigationRoot,
  createNavigation,
  createBottomTabsRoot,
  createNormalRoot,
  registerScreen,
  createScreens,
  defaultTheme,
  setTheme,
  getTheme,
  createSimpleTheme,
  setPreloadResult, // should not need this as it is done automatically

  // common hooks
  useParams,
  useNavigation,


  // extra
  useBottomTabIndex,
  useBottomTabBadges,
  useBottomTabRefresh,
  useTheme,
  useIsFocused,
  useFocus,// same as useNavigating
  useNavigating,
  useNavigated,
  useUnloaded,
  usePreloadResult // e.g. usePreloadResult(routes.PostScreen)

Do global stuff like updating badge

If you want to use a component globally with the navigation context pass them as children of the NavigationProvider

<NavigationProvider {...}>
  <UpdateBottomTabBadgeSubscriber />
</NavigationProvider>

Scroll fix on web version

On the web version we need to disable window scroll since else it will sometimes use windows scroll instead of the scrollview Add this to your css

body {
  overflow: hidden;
}

Contributing

See the contributing guide to learn how to contribute to the repository and the development workflow.

License

MIT

Checkout our other libraries