Skip to content

Setting Up React Navigation in Expo Web: A Practical Guide

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

Introduction

We have come a long way from the days where we had to write different code for different platforms. Today, we can write code once and run it anywhere using technologies like React Native.

React Native is a framework that allows us to write native apps for Android, iOS, and the web, using JavaScript and React. This makes it more interesting for teams that want to cut time/development cost to look into it.

We recently launched our Expo-Zustand-Styled Components showcase app here at This Dot Labs. We also showcased how we can use expo, zustand, and styled-components to build a React Native app that can run on Android, iOS, and the web all in one codebase.

In this article, we will be looking at React Navigation, how to configure Deep Linking to navigate to different screens, and handling dynamic routes paths in our app. We'll do this by looking at the challenges we had to deal with while working on our showcase app, especially when running the app on the web.

Getting started

Before we dive in, we need to install all the needed dependencies. Let's make sure we have the expo cli installed. We can do this by running the following command:

  npm i expo-cli

We can now Initialize a new Expo app. We can do this by running the following command:

    npx create-expo-app rn-web-routing

Next, we need to install the navigation package, and since we are also building this app for the web, we need to install the needed dependencies.

    npx expo install @react-navigation/native @react-navigation/native-stack react-dom react-native-web @expo/webpack-config

You can also leverage our expo-zustand-styled-component starter kit, which offers all configurations to build for IOS, Android, and the web.

Setting up the navigation

Let's set up our navigation routes. We will use native-stack for our navigation. We can do this by creating a navigation folder in our src folder, and creating an index.ts file in it. This will contain our root navigator.

import { createNativeStackNavigator } from "@react-navigation/native-stack";
import React from "react";
// import screens

const Stack = createNativeStackNavigator();

const Routes = () => {
  return <Stack.Navigator>{/** screens stack */}</Stack.Navigator>;
};

export default Routes;

We can now import our Routes component in our App.tsx file and render it.

    import React from 'react';
    import Routes from './navigation';

    export default function App() {
        return (
        {/** ... other config eg provider */}
        <Routes />
        {/** ... other config eg provider  */}
        );
    }

Creating our screens

We can now create our screens. We will create a screens folder in our src folder and a Home.tsx file in it which will contain our home screen.

import React from "react";
import { View, Text } from "react-native";

const Home = () => {
  return (
    <View>
      <Text>This is Home</Text>
    </View>
  );
};

export default Home;

We can now import our Home component in our Routes component, and add it to our stack.

import { createNativeStackNavigator } from "@react-navigation/native-stack";
import React from "react";
import Home from "../screens/Home";

const Stack = createNativeStackNavigator();

const Routes = () => {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Home" component={Home} options={{ title: "Home" }} />
    </Stack.Navigator>
  );
};

export default Routes;

We can now do the same for the other screens we want to add to our stack.

Deep linking

During the development of the expo showcase kit, we faced an issue on the web. When we navigate to a page, the URL in the browser still remains in the index. This is because we have not configured deep linking.

Deep Linking is a way to navigate to different screens in our app using a custom URL link. This is very useful when we want to have individual URLs for each of our pages. This is also useful when we want to share a link to a specific screen in our app, saving the user time and energy in locating a particular page themselves.

We can do this by creating a config file in our navigation folder with the following code.

import { NavigationContainer } from "@react-navigation/native";
import * as Linking from "expo-linking";

const linking = {
  prefixes: [Linking.createURL("/")], // this is the prefix for our app. Could be anything eg https://myapp.com
  config: {
    screens: {
      Home: "",
      //  ... other screens
    },
  },
};

export default linking;

And then we can import our linking config in our App.tsx file and pass it to the NavigationContainer.

    export default function App() {
        return (
        {/** ... other config eg provider */}
        <NavigationContainer linking={linking}>
            <Routes />
        </NavigationContainer>
        {/** ... other config eg provider  */}
        );
    }

We will come back to this config file later as we are going to deep dive into more complex routing, and how to handle it. Let's run our app now, and see how it works.

Running the app

Now, we can run our app. We can do this by running the following command:

    expo start

Press w to open it on a web browser, you should see something like this:

We can see we are in our index page which is the home page, we can now navigate to the eg Profile page by clicking the button on the home page. To have your preferred page url path, we need to update the linking config with the appropriate page URL. Otherwise, deep linking will use the screen name as the page URL.

Handling dynamic routes

Say you want to have a page that can be accessed by different users. You can do this by passing the user id as a parameter in the URL. This is called dynamic routing. We can do this by updating our linking config file with the following code.

import { NavigationContainer } from "@react-navigation/native";
import * as Linking from "expo-linking";

const linking = {
  prefixes: [Linking.createURL("/")],
  config: {
    screens: {
      Home: "",
      Profile: "profile/:id",
      //  ... other screens
    },
  },
};

export default linking;

This will allow us to access the Profile page by passing the user id as a parameter in the URL. We can now update our Profile page to get the user id from the URL and display it.

import React from "react";
import { View, Text } from "react-native";
import { useRoute } from "@react-navigation/native";

const Profile = () => {
  const route = useRoute();
  const { id } = route.params;
  return (
    <View>
      <Text>This is Profile</Text>
      <Text>User id: {id}</Text>
    </View>
  );
};

export default Profile;

During the development of our showcase app, we were faced with an issue. We needed to replicate a route /tree/main/a/b/c with a pattern as /tree/:branch/:path*, where the path is a/b/cjust like we have in other showcases.

import { NavigationContainer } from "@react-navigation/native";
import * as Linking from "expo-linking";

const linking = {
  prefixes: [Linking.createURL("/")],
  config: {
    screens: {
      Home: "",
      Profile: "profile/:id",
      Tree: "tree/:branch/:path*",
      //  ... other screens
    },
  },
};

export default linking;

This didn't work as expected. But on further investigation, we found out that we have to manually handle this route in the getStateFromPath of the linking configuration file, by updating the state and returning it.

import { getStateFromPath } from "react-native";
import { NavigationContainer } from "@react-navigation/native";
import * as Linking from "expo-linking";

const linking = {
  prefixes: [Linking.createURL("/")],
  config: {
    screens: {
      Home: "",
      Profile: "profile/:id",
      Tree: "tree/:branch/:path",
      //  ... other screens
    },
  },
  getStateFromPath(path, options) {
    let state = getStateFromPath(path, options);

    // If the state is undefined, it means that the path doesn't match any route in our config, and
    // we want to handle the path ourselves to the right screen, by updating the state and returning it.
    if (!state) {
      // check if route contains the main identifier of the screen we want to show
      const isTree = path.includes("tree");
      if (isTree) {
        const [, , branch, ...rest] = path.split("/");

        state = {
          routes: [
            {
              path,
              name: "Tree",
              params: {
                branch,
                path: rest.join("/"), // here, our path is the rest of the path after the branch
              },
            },
          ],
        };
      }
    }
    return state;
  },
};

export default linking;

With these configurations, we can now navigate throughout our app using the Link component from react-navigation/native.

import React from "react";
import { View, Text } from "react-native";
import { Link } from "@react-navigation/native";

const Home = () => {
  return (
    <View style={styles.container}>
      <Text>This is Home</Text>
      <View style={styles.button}>
        <Link to="profile">
          <Button title="Go to Profile" />
        </Link>
      </View>
      <View style={styles.button}>
        <Link to="tree/d1/d2/d3">
          <Button title="Go to Tree" />
        </Link>
      </View>
    </View>
  );
};

To make use of the path parameter in our Tree page:

import React from "react";
import { View, Text } from "react-native";

const Tree = ({ route }) => {
  const { branch, path } = route.params;
  return (
    <View>
      <Text>This is Tree</Text>
      <Text>Branch: {branch}</Text>
      <Text>Path: {path}</Text>
    </View>
  );
};

export default Tree;

Conclusion

In this article, we have seen how to set up our navigation in our expo app using react-navigation/native. We have also seen how to configure deep linking, which will help us to navigate through our app using the URL path. Next, we've learned how to handle dynamic routes, which will help us pass parameters in the URL path, and handle some special cases like the one we had with the Tree page.

If you have any questions or run into any trouble, feel free to join the discussions going on at starter.dev or on our Discord.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co