A Complete Guide to React Native Navigation

Allan Graves
Jan 8 · 14 min read

Every good app needs a good navigator. Just like a cross-country trip, a good navigator takes us from place to place in our app, ensuring that we get to the place we want to go. A bad navigator takes us… well, where the navigator wants to go. We get frustrated and put the app down, lowering user engagement.

I’m sure all of us remember the early days of Android where the use of the back button might take you to… the previous screen, a navigation menu, the previous app, or really anywhere! It was kind of fun — you’d click it and just see where you ended up, like a random Tinder date your friend set up for you.

React Native offers navigation built on top of React Navigation. They have an excellent source of docs here: https://reactnavigation.org/docs/getting-started

Most apps have pretty standard use cases:

  1. Bring the user to different screens in the app.
  2. Allow for the back button to bring the user back to a previous screen.
  3. Pass information from screen to screen, perhaps to filter or update a screen after navigating.
  4. Show a sub navigator or other navigation stack to allow for more choices on subscreens.

React Navigation easily meets all these use cases just by default.

With a React Native project, it’s pretty easy to get going. Follow the instructions here — https://reactnavigation.org/docs/getting-started. We’ll need to ensure that we have all the right packages. As always, it is best to do this with your packager \ Metro not running, and your emulator not running. Havig them running can result in strange errors later.

In addition to the basic react-native navigation setup, you’ll need to install the specific packages for your setup, depending on which Navigator you want to use.

To test out all Navigators, run the following:

npm install @react-navigation/native  react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view  @react-navigation/stack  @react-navigation/drawer  @react-navigation/bottom-tabs 

Once we do this, we need to wrap our top level App.js in a Navigation Container.

There’s not a lot going on here.

  1. In line 1 — we’ve added the gesture handlers. This is a new library that replaces the underlying React Native gesture handler. It fixes a number of issues with the underlying React Native responder system — a talk can be seen here: https://www.youtube.com/watch?v=V8maYc4R2G0. Note that this new library does not work on Windows or Web, so you may need the older library here, depending on your long term goal.
  2. Line 4 — we import that new navigation container.
  3. Line 11 — we wrap our current app in a navigation container. A navigation container is required — it sets up the state required for screens to persist, track which screen you are on, and for Android, provide for the back button API. With no navigator actually specified here, we’re not going to see anything different in our app, but it will run.
Image for post
Image for post

Now, let’s talk about the types of Navigators.

First, we’ll build 3 screens to use to show the differences.

And, after changing Line 17 to show <ScreenOne />, we have this:

Image for post
Image for post

Let’s take a quick look at what the different basic navigator types look like. There are 3 types we will be looking at today:

  • Stack Navigator
  • Drawer Navigator
  • Bottom Tab Navigator

We will not be looking at:

  • Native Stack Navigator — in all honesty, this is a fine navigator, and completely emulates the way that the native navigation APIs work. However, it does not work for Web at this point, so it can only be used if you want to deploy to Android and to iOS.
  • The 2 material navigators, bottom tab and top tab — these are extensions of the react-native-paper components, which are in turn extensions of the Bottom Tab Navigator from React Navigation. These are pretty much the same as the Tab Navigator above.

You may at this point assume that the Navigators are pretty similar — and extensible. That’s correct — you can make additional navigators by easily customizing the behavior that is in each navigator. Every navigator has the same basic components:

  • Routes — these are the configured screens that a user would see. As the user navigates through the App, the Navigator will save the differing state of each screen, as well as where the user has been. This enables things like using the back button.
  • Components — the configured screen or other React Component that will be displayed when a route is navigated to. A route can have properties passed with it, but a component is just a component. If the component doesn’t know what to do with the properties of a Route… it ignores them.
  • Properties — Properties that are passed down the chain of components to various components that will deal with them. In true React Native fashion, a Higher Order Component passes these to lower components to render.

The first navigator we’ll talk about is the Stack Navigator.

This navigator is exactly what it sounds like — a stack. It provides exactly what it sounds like — a stack of navigation. Your first item shows at the top, and each item after it is added on top of the stack.

We can easily do this with code like:

This code will display the following:

Image for post
Image for post

When you run the code, you may find that this code is not actually providing something you might expect in a navigation scenario — there’s no controls to move between routes.

Instead, the initial screen is displayed, and that is that.

To do that, for the Stack Navigator, we need to create our own navigation elements that move between screens.

Let’s change the Screen definitions to contain a button that navigates from route to route:

To do this, we’ll use the call ‘navigation.navigate’ on line 7,19,31. This API will take a text string ( a route name) that we previously configured by using ‘Stack.Screen name=XXX’. XXX is the name.

You’ll notice initially that there’s no navigation elements — just the Screen Header across the top of the screen. (Screen One, for our initial screen.)

As we move from element to element though — the Header bar changes — and puts in place a <- symbol so that we can navigate backwards. Notice that this arrow disappears when we arrive back at Screen One — the start of our stack. By default, the ‘back arrow’ on Android also works to go backwards, popping elements off the stack.

We are going to now add a new function to each of our Screens:

console.log(navigation.dangerouslyGetState());

This function will print out the state of the navigator as we move through the various screens.

Screen One:

01–05 14:42:16.776 4884 19212 I ReactNativeJS: index: 0,
01–05 14:42:16.776 4884 19212 I ReactNativeJS: routeNames: [ ‘One’, ‘Two’, ‘Three’ ],
01–05 14:42:16.776 4884 19212 I ReactNativeJS: routes:
01–05 14:42:16.776 4884 19212 I ReactNativeJS: [ { key: ‘One-W6–8fHZHIQHKWTVDe5pHj’,
01–05 14:42:16.776 4884 19212 I ReactNativeJS: name: ‘One’,
01–05 14:42:16.776 4884 19212 I ReactNativeJS: params: undefined } ] }

Notice only 1 route is defined at this point. Pressing the button to move to Screen Two brings this:

01–05 14:46:17.233 4884 19212 I ReactNativeJS: routes:
01–05 14:46:17.233 4884 19212 I ReactNativeJS: [ { key: ‘One-W6–8fHZHIQHKWTVDe5pHj’,
01–05 14:46:17.233 4884 19212 I ReactNativeJS: name: ‘One’,
01–05 14:46:17.233 4884 19212 I ReactNativeJS: params: undefined },
01–05 14:46:17.233 4884 19212 I ReactNativeJS: { key: ‘Two-Ij1hKIRjwMgqqekKtjt4B’,
01–05 14:46:17.233 4884 19212 I ReactNativeJS: name: ‘Two’,
01–05 14:46:17.233 4884 19212 I ReactNativeJS: params: undefined } ] }

Moving to Screen Two adds another screen to the stack.

However — moving back to Screen One … doesn’t print anything.

I wonder why?

To solve this, we need to discuss the rendering model of React.

React uses a delayed Render methodology with 2 characteristics:

  1. Only components that are changed are rendered. This allows for screens to be displayed quickly.
  2. Only screens that are displayed are rendered.

The React Lifecycle has methods to show these — the 2 we are interested in are componentDidMount() and componentWillUnmount().

However, since we’re using Functional Components, the new hotness, we have to use a useEffect hook for this.

To do that, we’ll add code similar to this to each of our functions:

This will print the routes as we move through our stack navigator.

And, looking at the debug output here, you can easily see that any time we pop the top off the stack of our navigation, we’ll see a screen unmount. For instance, when we use the back arrow to move from Screen Three to Screen Two, this shows up:

[Info] 01-05 16:29:47.151  4884  4212 I ReactNativeJS: Screen 3 Unmount:
[Info] 01-05 16:29:47.155 4884 4212 I ReactNativeJS: { stale: false,
01-05 16:29:47.155 4884 4212 I ReactNativeJS: type: 'stack',
01-05 16:29:47.155 4884 4212 I ReactNativeJS: key: 'stack-vo35QL6onuHzaoVKRkcLn',
01-05 16:29:47.155 4884 4212 I ReactNativeJS: routeNames: [ 'One', 'Two', 'Three' ],
01-05 16:29:47.155 4884 4212 I ReactNativeJS: index: 1,
01-05 16:29:47.155 4884 4212 I ReactNativeJS: routes:
01-05 16:29:47.155 4884 4212 I ReactNativeJS: [ { key: 'One-YQJCh-OSey7sY3F8g8LjH',
01-05 16:29:47.155 4884 4212 I ReactNativeJS: name: 'One',
01-05 16:29:47.155 4884 4212 I ReactNativeJS: params: undefined },
01-05 16:29:47.155 4884 4212 I ReactNativeJS: { key: 'Two-KTzPjo4zblaJ2NnwFuwlR',
01-05 16:29:47.155 4884 4212 I ReactNativeJS: name: 'Two',
01-05 16:29:47.155 4884 4212 I ReactNativeJS: params: undefined } ] }

Similarly, when we hit the button on Screen Three to move to Screen One, both Screen Two and Screen Three unmount, as they are no longer on the route stack.

Only Screen One does not unmount, because it is always at the base of the stack.

Or is it? Can we force it to unmount?

The stackAction reference API shows a ‘Replace’ call — to Replace the current route.

Let’s add a new button…

<Button
title="Replace with Three"
onPress={() => navigation.replace('Three')}
/>

And boom! Now when we click that button, we see the current Screen One unmount, and the only route on the stack is Screen Three:

[Info] 01–05 16:53:01.220 4884 12243 I ReactNativeJS: Screen 1 Unmount:
[Info] 01–05 16:53:01.221 4884 12243 I ReactNativeJS: { stale: false,
01–05 16:53:01.221 4884 12243 I ReactNativeJS: type: ‘stack’,
01–05 16:53:01.221 4884 12243 I ReactNativeJS: key: ‘stack-4NIFPEcHeiA1UQFsqkUwF’,
01–05 16:53:01.221 4884 12243 I ReactNativeJS: index: 0,
01–05 16:53:01.221 4884 12243 I ReactNativeJS: routeNames: [ ‘One’, ‘Two’, ‘Three’ ],
01–05 16:53:01.221 4884 12243 I ReactNativeJS: routes:
01–05 16:53:01.221 4884 12243 I ReactNativeJS: [ { key: ‘Three-XvOAF4N-eo8FbBDz981TA’,
01–05 16:53:01.221 4884 12243 I ReactNativeJS: name: ‘Three’,
01–05 16:53:01.221 4884 12243 I ReactNativeJS: params: undefined } ] }

The stack function has other components — including a push and a pop routine. I’ll let you explore those.

Why did we spend so much time on the Lifecycle of the stack?

Because every navigator has their own Lifecycle. This will be part of the important part of understanding navigators — knowing when components are mounted and unmounted.

Let’s move on to the drawer navigator!

The first thing to do is import the library:

import { createDrawerNavigator } from '@react-navigation/drawer';

Simple, easy peezy done.

Once that’s done, we’ll take our existing Screens, and wrap them in the navigator we just created. We still need to leave the Navigation Container available though — so all we are going to do is change the navigation definition to drawer.

I’m really bad at pulling the drawer out on the emulator. I’m also bad at Halo and Call of Duty.

So — what do our routes look like when dealing with the a drawer navigator? Remember, with the stack navigator, routes were only added to the routes when you navigated to them — it was a stack. I know… crazy.

With a drawer, all navigation routes are already defined… but in keeping with the lazy mount React paradigm…

[Info] 01–06 15:33:57.861 4884 8036 I ReactNativeJS: { stale: false,
01–06 15:33:57.861 4884 8036 I ReactNativeJS: index: 0,
01–06 15:33:57.861 4884 8036 I ReactNativeJS: routeNames: [ ‘One’, ‘Two’, ‘Three’ ],
01–06 15:33:57.861 4884 8036 I ReactNativeJS: history: [ { type: ‘route’, key: ‘One-TTViLKdH1xlcJHU4EW07q’ } ],
01–06 15:33:57.861 4884 8036 I ReactNativeJS: routes:
01–06 15:33:57.861 4884 8036 I ReactNativeJS: [ { name: ‘One’,
01–06 15:33:57.861 4884 8036 I ReactNativeJS: key: ‘One-TTViLKdH1xlcJHU4EW07q’,
01–06 15:33:57.861 4884 8036 I ReactNativeJS: params: undefined },
01–06 15:33:57.861 4884 8036 I ReactNativeJS: { name: ‘Two’,
01–06 15:33:57.861 4884 8036 I ReactNativeJS: key: ‘Two-ibrPGdV3D4cz3XsTNczLq’,
01–06 15:33:57.861 4884 8036 I ReactNativeJS: params: undefined },
01–06 15:33:57.861 4884 8036 I ReactNativeJS: { name: ‘Three’,
01–06 15:33:57.861 4884 8036 I ReactNativeJS: key: ‘Three-k8FbMgqZ8b6Ks17GF7q_S’,
01–06 15:33:57.861 4884 8036 I ReactNativeJS: params: undefined } ],
01–06 15:33:57.861 4884 8036 I ReactNativeJS: type: ‘drawer’,
01–06 15:33:57.861 4884 8036 I ReactNativeJS: key: ‘drawer-eljOWlZef — CmBSQuUt1p’ }

Notice that all routes are fully defined. There is no “back” here, which pops a route off the stack.

You can navigate to any item in the route list at any point in time.

Clicking our pre-existing buttons to move between screens still works… but if you click the “Replace with Three” button… you get… a Red Screen of Death!

[Info] 01-06 15:46:16.879  4884  8036 E ReactNativeJS: TypeError: navigation.replace is not a function. (In 'navigation.replace('Three')', 'navigation.replace' is undefined)

This isn’t a stack navigator, so you can’t replace any route with other routes.

On the other hand, we can clearly see that mounting works just fine:

[Info] 01-06 15:46:14.429  4884  8036 I ReactNativeJS: Screen 2 Mount:
[Info] 01-06 15:46:15.233 4884 8036 I ReactNativeJS: Screen 3 Mount:

One important difference here is that we have a history instead of routes. This way we can see how the user is moving between screens:

01-06 15:46:15.234  4884  8036 I ReactNativeJS:   history:
01-06 15:46:15.234 4884 8036 I ReactNativeJS: [ { type: 'route', key: 'One-TTViLKdH1xlcJHU4EW07q' },
01-06 15:46:15.234 4884 8036 I ReactNativeJS: { type: 'route', key: 'Two-ibrPGdV3D4cz3XsTNczLq' },
01-06 15:46:15.234 4884 8036 I ReactNativeJS: { type: 'route', key: 'Three-k8FbMgqZ8b6Ks17GF7q_S' } ] }

Let’s hide that button that doesn’t do anything while we’re here.

First, import useNavigationState from @react-navigation/native:

import {
NavigationContainer,
useNavigationState,
} from ‘@react-navigation/native’;

Next, turn ScreenOne into:

And our Button to use ‘Replace’ is gone — that happens at line 12 — we use the type of navigation to show or hide this button.

One interesting thing to note — a Drawer navigator does not unmount screens. If you push buttons to cycle through all screens, they will remain mounted.

The drawer screen is very useful for overall context navigation, and can be set to take varied widths. For instance, on a tablet, it could have a permanent navigator tab, while on a phone, require pulling open the drawer to navigate.

Our last type of Screen is the Tab Screen.

Go ahead and add the following to our app:

This creates a simple Tab Screen in our app.

We are using the bottom tab navigator — but the same thing applies to material top or bottom tabs, or any custom tab navigators.

Image for post
Image for post

Just like the drawer navigator, no unmounts are called — but screens are only mounted when the tab is actually rendered. Thus, after clicking through all the tabs, all tab screens are mounted, but only the top one is rendering changes. In fact, like the Drawer navigator, since our tabs don’t have any changing data, they are only rendered once — their rendering is static at that point.

Also like the drawer navigator, all routes are defined, and the history grows as the user navigates through the tabs.

This history is used to navigate “back”.

The next thing we need to discuss is passing data between screens.

For this, we’ll use some simple data, passing it from Screen One to Screen Two.

The next thing we need to do is pass data around our screens. In general, when doing this, we need to talk about what we are passing and why.

Different types of apps have different needs for passing data around.

In a navigation display that is hierarchical, the following might be true:

List of Items -> Detailed Info on Item -> Actions to complete on Item

In this case, passing information down the navigation pathway (as in a stack navigator), might be the right thing to do.

Consider though something more like this:

Screen One-> News Feed

Screen Two ->Logged in User Profile

In this example, passing information from Screen One to Screen Two isn’t worth it. A global context or global DB for logged in user would be worth while.

Once Screen Two is mounted, it will stay mounted. It would be up to the individual screen to refresh the information when it comes back into focus. We can use the useIsFocused hook to retrigger the information gathering and display for scenarios like this.

With that said, let’s pass some data from Screen One to Screen Four! This will emulate the first scenario — where data is passed down a series of components.

To do this, we need to first setup our navigation to pass parameters.

This is fairly easy to do — let’s add Screen Four:

We’ve maintained our basic useEffect hook here — this will let us see when things are mounted and unmounted.

We are also passing in {navigation, route} to the ScreenFour function — the addition of route will let us get the parameters for the route. We’ll use that later in the screen itself.

The screen will use {route.params.number} on line 14 to print this simple information out. In reality, you could pass anything you wanted to this, and display anything. Perhaps it is an itemID to display more info about the item itself — or a URL to query for information.

In Screen One, we’ll add this call:

<Button
title="Display Screen Four Info"
onPress={() => {
var rand = Math.random();
navigation.navigate('Four', {number: rand});
}}
/>

This allows us to pass a random number over to the new tab. It will only change the value though when the number changes — which *only* happens when using this button. Using the tab navigator (or another navigator) won’t change any values here — it will just display the last value rendered by the screen.

Image for post
Image for post

This value becomes part of the routes:

01-07 15:34:41.024  4884  4110 I ReactNativeJS:      { name: 'Four',
01-07 15:34:41.024 4884 4110 I ReactNativeJS: key: 'Four-0leyCbrbFyqXwhsLaSfKr',
01-07 15:34:41.024 4884 4110 I ReactNativeJS: params: { number: 0.6732985766145677 } } ],

However, using the button, we pass a new parameter, and React navigation triggers a new unmount and mount of the screen. React navigation saves the old state, and understands that the parameters have changed, and thus the screen needs to rerender.

You’ll find a function setParams — this is useful for changing the parameters from within the screen itself, of the screen you are on.

For instance, perhaps you pass a Title or User ID — and this screen allows the user to change that. Using setParams would update your navigation route Parameters to immediately show the new information, as well as update your routes so that upon coming back to the screen, there would be updated information. Otherwise, React would not render the screen again, as the params would not have changed.

As a short close to this article — displaying a sub-navigator is easy, but with a couple of gotchas. I’m going to point to the docs to cover this for me since this article is already getting quite long.

JavaScript In Plain English

New JavaScript + Web Development articles every day.

Allan Graves

Written by

Years of technology experience have given me a unique perspective on many things, including parenting, climate change, etc. Or maybe I’m just opinionated.

JavaScript In Plain English

New JavaScript + Web Development articles every day.

Allan Graves

Written by

Years of technology experience have given me a unique perspective on many things, including parenting, climate change, etc. Or maybe I’m just opinionated.

JavaScript In Plain English

New JavaScript + Web Development articles every day.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store