Skip to main content

React Navigation

This guide covers how routing works in an app built with Navify and React.

NavReactRouter uses the popular React Router library under the hood. With Navify and React Router, you can create multi-page apps with rich page transitions.

Everything you know about routing using React Router carries over into Navify React. Let's take a look at the basics of an Navify React app and how routing works with it.

Routing in Navify React

Here is a sample App component that defines a single route to the "/dashboard" URL. When you visit "/dashboard", the route renders the DashboardPage component.

App.tsx

const App: React.FC = () => (
<NavApp>
<NavReactRouter>
<NavRouterOutlet>
<Route path="/dashboard" component={DashboardPage} />
<Redirect exact from="/" to="/dashboard" />
</NavRouterOutlet>
</NavReactRouter>
</NavApp>
);

Directly after the Route, we define our default Redirect, which, when a user visits the root URL of the app ("/"), it redirects them to the "/dashboard" URL.

The redirect also has the exact prop set, which means the URL has to match the from prop (or the path prop if exact was used on a Route) precisely for this route to be a match. Without it, this redirect would render for every route, since every route begins with "/".

You can also programmatically redirect from a Route's render method based on a condition, like checking if a user is authed or not:

<Route
exact
path="/dashboard"
render={(props) => {
return isAuthed ? <DashboardPage {...props} /> : <LoginPage />;
}}
/>

The NavReactRouter component wraps the traditional BrowserRouter component from React Router, and sets the app up for routing. Therefore, use NavReactRouter in place of BrowserRouter. You can pass in any props to NavReactRouter and they will be passed down to the underlying BrowserRouter.

Nested Routes

Inside the Dashboard page, we define more routes related to this specific section of the app:

DashboardPage.tsx

const DashboardPage: React.FC = () => {
return (
<NavPage>
<NavRouterOutlet>
<Route exact path="/dashboard" component={UsersListPage} />
<Route path="/dashboard/users/:id" component={UserDetailPage} />
</NavRouterOutlet>
</NavPage>
);
};

Here, there are a couple more routes defined to point to pages from within the dashboard portion of the app. Note, that we need to define the whole route in the path, and we can't leave off "/dashboard" even though we arrived to this page from that URL. React Router requires full paths, and relative paths are not supported.

However, we can use the match objects url property to provide the URL that was matched to render a component, which helps when working with nested routes:

const DashboardPage: React.FC<RouteComponentProps> = ({ match }) => {
return (
<NavPage>
<NavRouterOutlet>
<Route exact path={match.url} component={UsersListPage} />
<Route path={`${match.url}/users/:id`} component={UserDetailPage} />
</NavRouterOutlet>
</NavPage>
);
};

Here, match.url contains the value of "/dashboard", since that was the URL used to render the DashboardPage.

These routes are grouped in an NavRouterOutlet, let's discuss that next.

The NavRouterOutlet component provides a container for Routes that render Navify "pages". When a page is in an NavRouterOutlet, the container controls the transition animation between the pages as well as controls when a page is created and destroyed, which helps maintain the state between the views when switching back and forth between them.

The DashboardPage above shows a users list page and a details page. When navigating between the two pages, the NavRouterOutlet provides the appropriate platform page transition and keeps the state of the previous page intact so that when a user navigates back to the list page, it appears in the same state as when it left.

An NavRouterOutlet should only contain Routes or Redirects. Any other component should be rendered either as a result of a Route or outside of the NavRouterOutlet.

Fallback Route

A common routing use case is to provide a "fallback" route to be rendered in the event the location navigated to does not match any of the routes defined.

We can define a fallback route by placing a Route component without a path property as the last route defined within an NavRouterOutlet.

DashboardPage.tsx

const DashboardPage: React.FC<RouteComponentProps> = ({ match }) => {
return (
<NavRouterOutlet>
<Route exact path={match.url} component={UsersListPage} />
<Route path={`${match.url}/users/:id`} component={UserDetailPage} />
<Route render={() => <Redirect to={match.url} />} />
</NavRouterOutlet>
);
};

Here, we see that in the event a location does not match the first two Routes the NavRouterOutlet will redirect the Navify React app to the match.url path.

You can alternatively supply a component to render instead of providing a redirect.

const DashboardPage: React.FC<RouteComponentProps> = ({ match }) => {
return (
<NavRouterOutlet>
<Route exact path={match.url} component={UsersListPage} />
<Route path={`${match.url}/users/:id`} component={UserDetailPage} />
<Route component={NotFoundPage} />
</NavRouterOutlet>
);
};

The NavPage component wraps each view in an Navify React app and allows page transitions and stack navigation to work properly. Each view that is navigated to using the router must include an NavPage component.

import { NavContent, NavHeader, NavPage, NavTitle, NavToolbar } from '@navify/react';
import React from 'react';

const Home: React.FC = () => {
return (
<NavPage>
<NavHeader>
<NavToolbar>
<NavTitle>Home</NavTitle>
</NavToolbar>
</NavHeader>
<NavContent className="nav-padding">Hello World</NavContent>
</NavPage>
);
};
export default Home;

There are several options available when routing to different views in an Navify React app. Here, the UsersListPage uses NavItem's routerLink prop to specify the route to go to when the item is tapped/clicked:

UsersListPage.tsx

const UsersListPage: React.FC = () => {
return (
<NavPage>
<NavHeader>
<NavToolbar>
<NavTitle>Users</NavTitle>
</NavToolbar>
</NavHeader>
<NavContent>
<NavList>
<NavItem routerLink="/dashboard/users/1">
<NavLabel>User 1</NavLabel>
</NavItem>
<NavItem routerLink="/dashboard/users/2">
<NavLabel>User 2</NavLabel>
</NavItem>
</NavList>
</NavContent>
</NavPage>
);
};

Other components that have the routerLink prop are NavButton, NavCard, NavRouterLink, NavFabButton, and NavItemOption.

Each of these components also have a routerDirection prop to explicitly set the type of page transition to use ("back", "forward", or "none").

Outside of these components that have the routerLink prop, you can also use React Routers Link component to navigate between views:

<Link to="/dashboard/users/1">User 1</Link>

We recommend using one of the above methods whenever possible for routing. The advantage to these approaches is that they both render an anchor (<a>)tag, which is suitable for overall app accessibility.

A programmatic option for navigation is using the history prop that React Router provides to the components it renders via routes.

<NavButton
onClick={(e) => {
e.preventDefault();
history.push('/dashboard/users/1');
}}
>
Go to User 1
</NavButton>
note

history is a prop.

React Router uses the history package which has a history.go method that allows developers to move forward or backward through the application history. Let's take a look at an example.

Say you have the following application history:

/pageA --> /pageB --> /pageC

If you were to call router.go(-2) on /pageC, you would be brought back to /pageA. If you then called router.go(2), you would be brought to /pageC.

Using history.go() in Navify React is not supported at the moment. Interested in seeing support for this get added to Navify React?

URL Parameters

The second route defined in the Dashboard Page has a URL parameter defined (the ":id" portion in the path). URL parameters are dynamic portions of the path, and when the user navigates to a URL such as "/dashboard/users/1", the "1" is saved to a parameter named "id", which can be accessed in the component the route renders. Let's see how that's done.

UserDetailPage.tsx

interface UserDetailPageProps
extends RouteComponentProps<{
id: string;
}> {}

const UserDetailPage: React.FC<UserDetailPageProps> = ({ match }) => {
return (
<NavPage>
<NavHeader>
<NavToolbar>
<NavTitle>User Detail</NavTitle>
</NavToolbar>
</NavHeader>
<NavContent>User {match.params.id}</NavContent>
</NavPage>
);
};

The match prop contains information about the matched route, including the URL params. We obtain the id param here and display it on the screen.

Note how we use a TypeScript interface to strongly type the props object. The interface gives us type safety and code completion inside of the component.

Linear Routing versus Non-Linear Routing

Linear Routing

If you have built a web app that uses routing, you likely have used linear routing before. Linear routing means that you can move forward or backward through the application history by pushing and popping pages.

The following is an example of linear routing in a mobile app:

The application history in this example has the following path:

Accessibility --> VoiceOver --> Speech

When we press the back button, we follow that same routing path except in reverse. Linear routing is helpful in that it allows for simple and predictable routing behaviors.

The downside of linear routing is that it does not allow for complex user experiences such as tab views. This is where non-linear routing comes into play.

Non-Linear Routing

Non-linear routing is a concept that may be new to many web developers learning to build mobile apps with Navify.

Non-linear routing means that the view that the user should go back to is not necessarily the previous view that was displayed on the screen.

The following is an example of non-linear routing:

In the example above, we start on the Originals tab. Tapping a card brings us to the Ted Lasso view within the Originals tab.

From here, we switch to the Search tab. Then, we tap the Originals tab again and are brought back to the Ted Lasso view. At this point, we have started using non-linear routing.

Why is this non-linear routing? The previous view we were on was the Search view. However, pressing the back button on the Ted Lasso view should bring us back to the root Originals view. This happens because each tab in a mobile app is treated as its own stack. The Working with Tabs sections goes over this in more detail.

If tapping the back button simply called history.go(-1) from the Ted Lasso view, we would be brought back to the Search view which is not correct.

Non-linear routing allows for sophisticated user flows that linear routing cannot handle. However, certain linear routing APIs such as history.go() cannot be used in this non-linear environment. This means that history.go() should not be used when using tabs or nested outlets.

Which one should I choose?

We recommend keeping your application as simple as possible until you need to add non-linear routing. Non-linear routing is very powerful, but it also adds a considerable amount of complexity to mobile applications.

The two most common uses of non-linear routing is with tabs and nested NavRouterOutlets. We recommend only using non-linear routing if your application meets the tabs or nested router outlet use cases.

For more on tabs, please see Working with Tabs.

For more on nested router outlets, please see Nested Routes.

Shared URLs versus Nested Routes

A common point of confusion when setting up routing is deciding between shared URLs or nested routes. This part of the guide will explain both and help you decide which one to use.

Shared URLs

Shared URLs is a route configuration where routes have pieces of the URL in common. The following is an example of a shared URL configuration:

const App: React.FC = () => {
<NavApp>
<NavReactRouter>
<NavRouterOutlet>
<Route path="/dashboard" exact={true}>
<DashboardMainPage />
</Route>
<Route path="/dashboard/stats" exact={true}>
<DashboardStatsPage />
</Route>
</NavRouterOutlet>
</NavReactRouter>
</NavApp>
}

The above routes are considered "shared" because they reuse the dashboard piece of the URL.

Nested Routes

Nested Routes is a route configuration where routes are listed as children of other routes. The following is an example of a nested route configuration:

const App: React.FC = () => (
<NavApp>
<NavReactRouter>
<NavRouterOutlet>
<Route path="/dashboard/:id">
<DashboardRouterOutlet />
</Route>
</NavRouterOutlet>
</NavReactRouter>
</NavApp>
)

const DashboardRouterOutlet: React.FC = () => (
<NavRouterOutlet>
<Route path="/dashboard" exact={true}>
<DashboardMainPage />
</Route>
<Route path="/dashboard/stats" exact={true}>
<DashboardStatsPage />
</Route>
</NavRouterOutlet>
)

The above routes are nested because they are in the children array of the parent route. Notice that the parent route renders the DashboardRouterOutlet component. When you nest routes, you need to render another instance of NavRouterOutlet.

Which one should I choose?

Shared URLs are great when you want to transition from page A to page B while preserving the relationship between the two pages in the URL. In our previous example, a button on the /dashboard page could transition to the /dashboard/stats page. The relationship between the two pages is preserved because of a) the page transition and b) the url.

Nested routes should be used when you want to render content in outlet A while also rendering sub-content inside of a nested outlet B. The most common use case you will run into is tabs. When you load up a tabs Navify starter application, you will see NavTabBar and NavTabs components rendered in the first NavRouterOutlet. The NavTabs component renders another NavRouterOutlet which is responsible for rendering the contents of each tab.

There are very few use cases in which nested routes make sense in mobile applications. When in doubt, use the shared URL route configuration. We strongly caution against using nested routing in contexts other than tabs as it can quickly make navigating your app confusing.

Working with Tabs

When working with tabs, Navify needs a way to know which view belongs to which tab. The NavTabs component comes in handy here, but let's look at what the routing setup for this looks like:

<NavApp>
<NavReactRouter>
<NavRouterOutlet>
<Route path="/tabs" render={() => <Tabs />} />
<Route exact path="/">
<Redirect to="/tabs" />
</Route>
</NavRouterOutlet>
</NavSplitPane>
</NavReactRouter>
</NavApp>

Here, our tabs path loads a Tabs component. We provide each tab as a route object inside of this component. In this example, we call the path tabs, but this can be customized.

Let's start by taking a look at our Tabs component:

import { Redirect, Route } from 'react-router-dom';
import {
NavContent,
NavIcon,
NavLabel,
NavRouterOutlet,
NavTabBar,
NavTabButton,
NavTabs
} from '@navify/react';
import { NavReactRouter } from '@navify/react-router';
import { ellipse, square, triangle } from 'navicons/icons';
import Tab1 from './pages/Tab1';
import Tab2 from './pages/Tab2';
import Tab3 from './pages/Tab3';

const Tabs: React.FC = () => (
<NavContent>
<NavTabs>
<NavRouterOutlet>
<Redirect exact path="/tabs" to="/tabs/tab1" />
<Route exact path="/tabs/tab1">
<Tab1 />
</Route>
<Route exact path="/tabs/tab2">
<Tab2 />
</Route>
<Route path="/tabs/tab3">
<Tab3 />
</Route>
<Route exact path="/tabs">
<Redirect to="/tabs/tab1" />
</Route>
</NavRouterOutlet>
<NavTabBar slot="bottom">
<NavTabButton tab="tab1" href="/tabs/tab1">
<NavIcon icon={triangle} />
<NavLabel>Tab 1</NavLabel>
</NavTabButton>
<NavTabButton tab="tab2" href="/tabs/tab2">
<NavIcon icon={ellipse} />
<NavLabel>Tab 2</NavLabel>
</NavTabButton>
<NavTabButton tab="tab3" href="/tabs/tab3">
<NavIcon icon={square} />
<NavLabel>Tab 3</NavLabel>
</NavTabButton>
</NavTabBar>
</NavTabs>
</NavContent>
);

export default Tabs;

If you have worked with Navify Framework before, this should feel familiar. We create an NavTabs component and provide an NavTabBar. The NavTabBar provides NavTabButton components, each with a tab property that is associated with its corresponding tab in the router config. We also provide an NavRouterOutlet to give NavTabs an outlet to render the different tab views in.

How Tabs in Navify Work

Each tab in Navify is treated as an individual navigation stack. This means if you have three tabs in your application, each tab has its own navigation stack. Within each stack you can navigate forwards (push a view) and backwards (pop a view).

This behavior is important to note as it is different than most tab implementations that are found in other web based UI libraries. Other libraries typically manage tabs as one single history stack.

Since Navify is focused on helping developers build mobile apps, the tabs in Navify are designed to match native mobile tabs as closely as possible. As a result, there may be certain behaviors in Navify's tabs that differ from tabs implementations you have seen in other UI libraries. Read on to learn more about some of these differences.

Child Routes within Tabs

When adding additional routes to tabs you should write them as sibling routes with the parent tab as the path prefix. The example below defines the /tabs/tab1/view route as a sibling of the /tabs/tab1 route. Since this new route has the tab1 prefix, it will be rendered inside of the Tabs component, and Tab 1 will still be selected in the NavTabBar.

<NavTabs>
<NavRouterOutlet>
<Redirect exact path="/tabs" to="/tabs/tab1" />
<Route exact path="/tabs/tab1">
<Tab1 />
</Route>
<Route exact path="/tabs/tab1/view">
<Tab1View />
</Route>
<Route exact path="/tabs/tab2">
<Tab2 />
</Route>
<Route path="/tabs/tab3">
<Tab3 />
</Route>
<Route exact path="/tabs">
<Redirect to="/tabs/tab1" />
</Route>
</NavRouterOutlet>
<NavTabBar slot="bottom">
<NavTabButton tab="tab1" href="/tabs/tab1">
<NavIcon icon={triangle} />
<NavLabel>Tab 1</NavLabel>
</NavTabButton>
<NavTabButton tab="tab2" href="/tabs/tab2">
<NavIcon icon={ellipse} />
<NavLabel>Tab 2</NavLabel>
</NavTabButton>
<NavTabButton tab="tab3" href="/tabs/tab3">
<NavIcon icon={square} />
<NavLabel>Tab 3</NavLabel>
</NavTabButton>
</NavTabBar>
</NavTabs>

Switching Between Tabs

Since each tab is its own navigation stack, it is important to note that these navigation stacks should never interact. This means that there should never be a button in Tab 1 that routes a user to Tab 2. In other words, tabs should only be changed by the user tapping a tab button in the tab bar.

A good example of this in practice is the iOS App Store and Google Play Store mobile applications. These apps both provide tabbed interfaces, but neither one ever routes the user across tabs. For example, the "Games" tab in the iOS App Store app never directs users to the "Search" tab and vice versa.

Let's take a look at a couple common mistakes that are made with tabs.

A Settings Tab That Multiple Tabs Reference

A common practice is to create a Settings view as its own tab. This is great if developers need to present several nested settings menus. However, other tabs should never try to route to the Settings tab. As we mentioned above, the only way that the Settings tab should be activated is by a user tapping the appropriate tab button.

If you find that your tabs need to reference the Settings tab, we recommend making the Settings view a modal by using nav-modal. This is a practice found in the iOS App Store app. With this approach, any tab can present the modal without breaking the mobile tabs pattern of each tab being its own stack.

The example below shows how the iOS App Store app handles presenting an "Account" view from multiple tabs. By presenting the "Account" view in a modal, the app can work within the mobile tabs best practices to show the same view across multiple tabs.

Reusing Views Across Tabs

Another common practice is to present the same view in multiple tabs. Developers often try to do this by having the view contained in a single tab, with other tabs routing to that tab. As we mentioned above, this breaks the mobile tabs pattern and should be avoided.

Instead, we recommend having routes in each tab that reference the same component. This is a practice done in popular apps like Spotify. For example, you can access an album or podcast from the "Home", "Search", and "Your Library" tabs. When accessing the album or podcast, users stay within that tab. The app does this by creating routes per tab and sharing a common component in the codebase.

The example below shows how the Spotify app reuses the same album component to show content in multiple tabs. Notice that each screenshot shows the same album but from a different tab.

Home TabSearch Tab

When working in a tabs view, Navify React needs a way to determine what views belong to which tabs. We accomplish this by taking advantage of the fact that the paths provided to a Route are regular expressions.

While the syntax looks a bit strange, it is reasonably straightforward once you understand it.

For example, the routes for a view with two tabs (sessions and speakers) can be set up as such:

<NavRouterOutlet>
<Route path="/:tab(sessions)" component={SessionsPage} exact={true} />
<Route path="/:tab(sessions)/:id" component={SessionDetail} />
<Route path="/:tab(speakers)" component={SpeakerList} exact={true} />
</NavRouterOutlet>

If the navigated URL were "/sessions", it would match the first route and add a URL parameter named "tab" with the value of "sessions" to the resulting match object passed into SessionsPage.

When a user navigates to a session detail page ("/sessions/1" for instance), the second route adds a URL parameter named "tab" with a value of "sessions". When NavRouterOutlet sees that both pages are in the same "sessions" tab, it provides an animated page transition to the new view. If a user navigates to a new tab ("speakers" in this case), NavRouterOutlet knows not to provide the animation.

Switches in NavRouterOutlet

Since NavRouterOutlet takes over the job in determining which routes get rendered, using a Switch from React Router has no effect when used inside of an NavRouterOutlet. Switches still function as expected when used outside an NavRouterOutlet.

More Information

For more info on routing in React using React Router, check out their docs at https://reacttraining.com/react-router/web.