Bridging the gap between web & native with Expo router
Tags: reactnative, react, javascript, webdev
Published: 2022-10-26 21:06:00

---
title: Bridging the gap between web & native with Expo router
published: true
description: Expo Router brings the best routing concepts from the web to native iOS and Android apps. Let's build a simple magazine app with it
tags: reactnative, react, js, webdev
cover_image: https://i.imgur.com/OIVss2B.png
published_at: 2022-10-26 21:06 +0000
canonical_url: https://blog.spirokit.com/bridging-the-gap-between-web-native-with-expo-router
---

## Intro

Historically speaking, trying to handle navigation on a universal app (that targets web and mobile) was a pain in the ass.

Navigation on the web usually is quite simple, and Next.js did a fantastic job with its file system-based router.

In mobile land, things are not that simple. [Fernando Rojo](https://twitter.com/FernandoTheRojo) did a fantastic job with [Solito](https://solito.dev/), which is a wrapper around React Navigation and Next.js that lets you share navigation code across platforms. This project gained a lot of traction, and the community started to work on solving the problem of navigation for universal apps.

### Introducing Expo Router

I heard about Expo Router a few months ago, and I instantly loved the concept:
What if we could have something like Next.js file-system-based router, but for universal apps? That is Expo Router.

### What's the big deal about this library?

> "Expo Router brings the best routing concepts from the web to native iOS and Android apps. Every file in the app directory automatically becomes a route in your mobile navigation, making it easier than ever to build, maintain, and scale your project."

If you've had to deal with deep linking on your mobile apps in the past, you already know that this is another major pain.

Expo Router was built on top of [React Navigation](https://reactnavigation.org/), and the entire deep linking system is automatically generated, meaning that you can share the same link on the web and mobile, and the deep link will automatically work 🤯.

No more weird mapping and matching routes.

There are tons of additional features like Offline support, but if you want to learn more about all these features, here's the [official docs](https://expo.github.io/router/docs)

> Given that this library is still in beta, some links may change

---

## Building a magazine app

![](https://i.imgur.com/AyzZKZt.png)

I wanted to test features like tab and stack navigation to get a first sense of how it feels to work on a real app using Expo Router, so I decided to build a very simple magazine app that shows a list of news and allows the user to navigate to each news to read more about it.

For this demo app, I'll be using [SpiroKit](https://bit.ly/3tGv8sy), which is a React Native UI kit I built. Given that is a paid product, feel free to follow this tutorial with your own UI.

[![](https://i.imgur.com/MyOHaEv.png)](https://bit.ly/3tGv8sy)

---

### Project setup

#### With SpiroKit

If you've decided to use SpiroKit, follow these steps to quickly generate a new expo project with SpiroKit and Expo Router:

1. Get your SpiroKit license [here](https://bit.ly/3tGv8sy).

2. Create a new project using the template

```sh
expo init my-app --template @spirokit/expo-router-template
```

3. Add SpiroKit to your project: Download the `spirokit-core-[version].tgz` file from Gumroad and add it to the root of your project.

Install the package by running the following command:

```sh
yarn add ./spirokit-core-[version].tgz
```

#### With your own UI

Run the following command to create a new project with expo-router:

```sh
npx create-react-native-app -t with-router
```

### First use

After creating my first project using the expo template, I just run `yarn start`.

By default, the starter templates don't include the app folder, so there is nothing to show. But we will get a friendly welcome message that will let us create our first route by only clicking the “touch app/index.js” button

![welcome message from Expo for an empty app without routes](https://i.imgur.com/xiKuQcv.png)

After clicking the button, I instantly got an update on all my devices (both web and mobile)

![welcome message from Expo after creating the first route](https://i.imgur.com/Lyp2qkc.png)

After returning to my code, I confirmed that the new `app/index.js` file was created.

```jsx
// app/index.js
import { Link, Stack } from "expo-router";
import { StyleSheet, Text, View } from "react-native";

export default function Page() {
  return (
    <View style={styles.container}>
      <View style={styles.main}>
        <Text style={styles.title}>Hello World</Text>
        <Text style={styles.subtitle}>This is the first page of your app.</Text>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "center",
    padding: 24,
  },
  main: {
    flex: 1,
    justifyContent: "center",
    maxWidth: 960,
    marginHorizontal: "auto",
  },
  title: {
    fontSize: 64,
    fontWeight: "bold",
  },
  subtitle: {
    fontSize: 36,
    color: "#38434D",
  },
});
```

Given that Expo Router is file-system based, we'll need to create new directories and files based on our needs.

My magazine app will use a bottom tab navigation with 2 main sections:

- News
- Settings

At the same time, the "News" section will use a Stack navigator to allow us to navigate to the details page. More about this below.

Let's start building our app!

### 1. Adding the Tab navigator

We need to add a tab navigator so we can navigate between the "news" and "settings" tabs.

Expo Router includes a feature called "Layout Routes". From the [official docs](https://expo.github.io/router/docs/features/layout-routes):

> "To render shared navigation elements like a header, tab bar, or drawer, you can use a Layout Route. If a directory contains a file named \_layout.js, it will be used as the layout component for all the sibling files in the directory."

Let's create our `app/_layout.js` and add the Tab navigation we need:

```jsx
// app/_layout.js
import { SpiroKitProvider, usePoppins, useSpiroKitTheme } from "@spirokit/core";
import { Tabs } from "expo-router";

// Setting up some global preferences for theming
const theme = useSpiroKitTheme({
  config: {
    colors: {
      primaryGray: "coolGray",
      primaryDark: "coolDark",
    },
  },
});

export default function Layout() {
  const fontLoaded = usePoppins();

  if (!fontLoaded) return <></>;
  return (
    {/* Required for SpiroKit */}
    <SpiroKitProvider theme={theme}>
      {/* This will add the Tabs navigator */}
      <Tabs screenOptions={{ headerShown: false }}>
        {/* This allows us to further customize any given route */}
        <Tabs.Screen
          name="index"
          options={{
            // This tab will no longer show up in the tab bar.
            href: null,
          }}
        />
      </Tabs>
    </SpiroKitProvider>
  );
}
```

> I'm excluding the index route from the Tab Bar by setting the href to null. I just want "news" and "settings" to be included in the Tab Bar.

### 2. Updating the `app/index.js` file

With the tabs navigator in place, I wanted to have a welcome screen (`app/index.js`), with a button that redirects to the news section.

Expo Router provides many options to move between routes, but given that it's still in beta, some things may not be supported yet. In this case, I'm using the `useLink` hook to move between routes.

```jsx
// app/index.js
import * as React from "react";
import { useLink } from "expo-router";
import { HomeIcon } from "react-native-heroicons/outline";

import { Button, Image, LargeTitle, VStack } from "@spirokit/core";
export default function Page() {
  // The useLink hook allows us to navigate between routes
  const link = useLink();
  return (
    <VStack
      space={4}
      justifyContent="center"
      alignItems={"center"}
      flex={1}
      backgroundColor={{
        linearGradient: {
          colors: ["primary.600", "emerald.800"],
          start: [0, 1],
          end: [1, 0],
        },
      }}
    >
      <Image
        width={64}
        borderWidth={8}
        borderColor="primary.500"
        height={64}
        borderRadius="full"
        source={{
          uri: "https://images.pexels.com/photos/1369476/pexels-photo-1369476.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1",
        }}
      ></Image>
      <LargeTitle color="white" width={"1/2"} textAlign="center">
        Welcome to magazine App
      </LargeTitle>

      {/* Here I'm using the onPress event to trigger the navigation */}
      {/* This won't work until we create the news route below */}
      <Button
        variant="secondary"
        textColor="white"
        colorMode={"dark"}
        IconLeftComponent={HomeIcon}
        width="auto"
        onPress={() => link.push("news")}
      >
        Home
      </Button>
    </VStack>
  );
}
```

After these changes, the welcome screen should look like this:

![Welcome screen after applying the UI updates](https://i.imgur.com/yQ31mVe.png)

### 3. Adding the "News" and "Settings" tabs

Let's start by creating the `app/news` and `app/settings` directories.

```sh
mkdir news
mkdir settings
```

Your project should look like this:

```bash
├── app
│   ├── index.js
│   ├── _layout.js
│   ├── news
│   │   ├── Empty directory
│   └── settings
│   │   ├── Empty directory
├── app.json
├── babel.config.js
├── index.js
├── package.json
├── README.md
└── yarn.lock
```

We'll also need to define which navigator to use on each tab. In this case, I decided to use stack navigators on each tab.
Let's create the `index.js` and `_layout.js` files inside "news" and "settings" directories:

```
touch ./app/news/index.js ./app/news/_layout.js ./app/settings/index.js ./app/settings/_layout.js
```

Now, your project structure should look like this:

```bash
├── app
│   ├── index.js
│   ├── _layout.js
│   ├── news
│   │   ├── index.js
│   │   └── _layout.js
│   └── settings
│       ├── index.js
│       └── _layout.js
├── app.json
├── babel.config.js
├── index.js
├── package.json
├── README.md
└── yarn.lock
```

To add the stack navigators, add this to the `app/news/_layout.js` and `app/settings/_layout.js` files:

```jsx
// app/news/_layout.js
// app/settings/_layout.js
import { Stack } from "expo-router";

export default function Layout() {
  return (
    <Stack screenOptions={{ headerShown: false }}></Stack>;
  )
}
```

Finally, let's customize the `app/news/index.js` and `app/settings/index.js` files to include a simple message:

```jsx
// app/news/index.js
import { Center, LargeTitle } from "@spirokit/core";

export default function News() {
  return (
    <Center flex={1}>
      <LargeTitle>News route</LargeTitle>
    </Center>
  )
}

// app/settings/index.js
import { Center, LargeTitle } from "@spirokit/core";

export default function News() {
  return (
    <Center flex={1}>
      <LargeTitle>Settings route</LargeTitle>
    </Center>
  )
}
```

You should now be able to navigate between tabs 🎉

### 4. Adding UI to the "News" Route

Let's add some UI to our `news` route. Don't worry if you are not using [SpiroKit](https://bit.ly/3tGv8sy). The key takeaway here is that we'll use the `useLink` hook from Expo Router to navigate to the news details route.

```jsx
// app/news/index.js

import {
  Button,
  VerticalCard,
  Badge,
  Avatar,
  TitleThree,
  Subhead,
  Image,
  Footnote,
  Box,
  HorizontalCard,
  VStack,
  HStack,
  LargeTitle,
  useColorModeValue,
  Pressable,
} from "@spirokit/core";
import { useLink } from "expo-router";
import { ScrollView } from "react-native";
import { BellIcon, LightBulbIcon } from "react-native-heroicons/outline";

export default function News() {
  const link = useLink();

  return (
    <Box
      flex={1}
      backgroundColor={useColorModeValue("white", "primaryDark.1")}
      safeArea
    >
      <ScrollView>
        <VStack space={4} flex={1} padding={4}>
          <HStack
            space={4}
            justifyContent="space-between"
            alignItems={"center"}
          >
            <LargeTitle>News</LargeTitle>
            <Button IconLeftComponent={BellIcon} size="sm" width="auto">
              Subscribe
            </Button>
          </HStack>
          {/* We are using the push method to navigate to news details */}
          <Pressable onPress={() => link.push("/news/1234")}>
            <MainTravelCard></MainTravelCard>
          </Pressable>

          <SecondaryTravelCard></SecondaryTravelCard>
          <FoodCard></FoodCard>
        </VStack>
      </ScrollView>
    </Box>
  );
}

const MainTravelCard = () => {
  return (
    <VerticalCard
      BadgeComponent={<Badge>Travel</Badge>}
      UserAvatarComponent={
        <Avatar
          alt="Siv Marko profile image"
          source={{ uri: "https://i.imgur.com/pfR8Ytj.png" }}
        ></Avatar>
      }
      userName="Siv Marko"
      TitleComponent={
        <TitleThree>Resting place of Australia's last convict ship</TitleThree>
      }
      DescriptionComponent={
        <Subhead>
          Wellington, New Zealand (CNN) - The storm that struck the Edwin Fox on
          February 1873 might sound dramatic.
        </Subhead>
      }
      DateComponent={<Footnote>15th June 2021</Footnote>}
      AssetComponent={
        <Image
          source={{ uri: "https://i.imgur.com/1lSpdz3.png" }}
          alt="Image of a ship"
        ></Image>
      }
    ></VerticalCard>
  );
};

const SecondaryTravelCard = () => {
  return (
    <HorizontalCard
      UserAvatarComponent={
        <Avatar
          alt="Kenny Grimes profile image"
          source={{ uri: "https://i.imgur.com/mwax0m0.png" }}
        ></Avatar>
      }
      userName="Kenny Grimes"
      TitleComponent={
        <TitleThree>
          Emirates introduces digital health verification for UAE passengers
        </TitleThree>
      }
      DescriptionComponent={
        <Subhead>
          Emirates and the Dubai Health Authority (DHA) have begun to implement
          full digital verification of Covid-19 medical records
        </Subhead>
      }
      DateComponent={<Footnote>15th June 2021</Footnote>}
      AssetLeftComponent={
        <Image
          source={{ uri: "https://i.imgur.com/EflHxyi.png" }}
          alt="Image of a ship"
        ></Image>
      }
    ></HorizontalCard>
  );
};

const FoodCard = () => {
  return (
    <HorizontalCard
      UserAvatarComponent={
        <Avatar
          alt="Paula Green profile image"
          source={{ uri: "https://i.imgur.com/Vbzbh6Z.png" }}
        ></Avatar>
      }
      userName="Paula Green"
      TitleComponent={
        <TitleThree>
          The Best Marinara Sauce You Can Get At The Store
        </TitleThree>
      }
      DateComponent={<Footnote>15th June 2021</Footnote>}
      AssetRightComponent={LightBulbIcon}
    ></HorizontalCard>
  );
};
```

Expected result:
![News home screen finished](https://i.imgur.com/IkV5Lnc.png)

### 5. Adding UI to the "Settings" Route (Optional)

I wanted to use the `settings` route to test if I could support switching between light and dark modes with Expo Router.

Following the Expo Router docs, I learned about the <RootContainer> component, which allows us to interact with the NavigationContainer (managed by Expo Router) to set things like "theme".

Let's customize the UI to add a switch that allows us to toggle dark mode:

```jsx
// app/settings/index.js
import {
  Body,
  Box,
  HStack,
  LargeTitle,
  Switch,
  useColorModeValue,
  VStack,
  useColorMode,
  Button,
  Input,
  Subhead,
} from "@spirokit/core";
import { RootContainer } from "expo-router";
import { DarkTheme, LightTheme } from "@react-navigation/native";
import { LogoutIcon, UserIcon, LinkIcon } from "react-native-heroicons/outline";

export default function Settings() {
  const { toggleColorMode, colorMode } = useColorMode();

  return (
    <Box flex={1}>
      {/* I'm using the global colorMode prop provided by SpiroKit to dinamically set the theme */}
      <RootContainer theme={colorMode === "light" ? LightTheme : DarkTheme} />
      {/* Header */}
      <Box
        safeAreaTop
        justifyContent={"flex-end"}
        padding={4}
        {/* I'm using the `useColorModeValue` hook provided by SpiroKit to set different colors for light and dark mode */}
        backgroundColor={useColorModeValue("primary.500", "primary.300")}
        minHeight={32}
      >
        <LargeTitle color={useColorModeValue("white", "primaryGray.900")}>
          Settings
        </LargeTitle>
      </Box>

      {/* Body */}
      <Box
        flex={1}
        backgroundColor={useColorModeValue("white", "primaryDark.1")}
        padding={4}
      >
        <VStack space={4} flex={1}>
          <HStack justifyContent={"space-between"} alignItems="center">
            <Body flex={1}>Dark mode</Body>
            <Switch onValueChange={toggleColorMode}></Switch>
          </HStack>
          <Input
            IconLeftComponent={UserIcon}
            LabelComponent={<Subhead>Name</Subhead>}
            defaultValue="Mauro"
          ></Input>
          <Input
            IconLeftComponent={UserIcon}
            LabelComponent={<Subhead>Lastname</Subhead>}
            defaultValue="Garcia"
          ></Input>
          <Input
            IconLeftComponent={LinkIcon}
            isDisabled
            LabelComponent={<Subhead>Twitter handle</Subhead>}
            defaultValue="https://www.twitter.com/mauro_codes"
          ></Input>
        </VStack>

        <Button IconLeftComponent={LogoutIcon}>Logout</Button>
      </Box>
    </Box>
  );
}
```

If everything goes well, we should now be able to toggle between light and dark mode

![settings route on light and dark mode](https://i.imgur.com/DoEdyVP.png)

### 6. Adding a dynamic route for the news details screen

From the [Expo docs](https://expo.github.io/router/docs/features/dynamic-routes):

> "Dynamic routes match any unmatched path at a given segment level. For example, /blog/[id] is a dynamic route. The variable part ([id]) is called a "slug"."

We are going to use this pattern to navigate from the "news" route to the news details ("news"/[id]).

Remember, Expo Router is based on your file system, so let's start by creating a new file for this dynamic route:

```bash
touch ./app/news/[id].js
```

Inside our new `[id].js` file, let's add some UI and see how we can access the `id` param.

> Given this is a demo, I'm using hardcoded data. In real life, we would use the id from the URL to request the information using `fetch` or `axios`.

```jsx
import { useLink } from "expo-router";
import { Platform, ScrollView } from "react-native";
import {
  Avatar,
  Subhead,
  Image,
  Box,
  VStack,
  HStack,
  LargeTitle,
  useColorModeValue,
  ZStack,
  Body,
  Button,
} from "@spirokit/core";
import { ChevronLeftIcon } from "react-native-heroicons/outline";

export default function NewsDetails({ route }) {
  // Extracting the id param from the route
  const id = route.params.id;

  // This should be replaced by real data coming from an external API
  const content = loremIpsum;

  return (
    <Box flex={1} backgroundColor={useColorModeValue("white", "primaryDark.1")}>
      <Header></Header>
      <ScrollView>
        <VStack space={4} flex={1} padding={4}>
          <Body>{content}</Body>
        </VStack>
      </ScrollView>
    </Box>
  );
}

const Header = () => {
  const link = useLink();

  return (
    <>
      <ZStack minHeight={56} overflow="hidden" width="full">
        <Image
          height={56}
          width="full"
          resizeMode="cover"
          source={{ uri: "https://i.imgur.com/1lSpdz3.png" }}
          alt="Image of a ship"
        ></Image>
        <VStack
          justifyContent={"space-between"}
          backgroundColor={"black:alpha.40"}
          padding={4}
          width="full"
          height={"full"}
        >
          {Platform.OS === "web" ? (
            <Button
              size="sm"
              width="auto"
              alignSelf={"flex-start"}
              onPress={() => link.back()}
              IconLeftComponent={ChevronLeftIcon}
            ></Button>
          ) : null}
          <LargeTitle numberOfLines={3} color={"white"}>
            Resting place of Australia's last convict ship
          </LargeTitle>
        </VStack>
      </ZStack>
      <AuthorLine></AuthorLine>
    </>
  );
};

const AuthorLine = () => {
  return (
    <HStack
      padding={4}
      justifyContent={"space-between"}
      alignItems={"center"}
      space={4}
    >
      <HStack space={4} flex={1} alignItems="center">
        <Avatar
          size={"sm"}
          alt="Siv Marko profile image"
          source={{ uri: "https://i.imgur.com/pfR8Ytj.png" }}
        ></Avatar>
        <Body>Siv Marko</Body>
      </HStack>
      <Subhead flex={1} textAlign="right">
        15th June 2021
      </Subhead>
    </HStack>
  );
};

const loremIpsum = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus blandit faucibus justo, at eleifend sapien pellentesque ut. Curabitur ultrices eget arcu sit amet luctus. Mauris accumsan ut mauris eget pharetra. Quisque dignissim sed leo sed condimentum. Nullam ligula nisi, pellentesque sit amet lacus eget, malesuada tempus lorem. Pellentesque fringilla erat a faucibus semper. Sed posuere tristique vulputate.

Nam convallis tempor dictum. Donec maximus nisl a tempor condimentum. Fusce egestas velit id ante consectetur feugiat. Nam non ligula metus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Fusce efficitur tellus leo, non vehicula lorem ornare id. Vivamus enim mauris, volutpat ut luctus semper, hendrerit eu dolor. Nunc a sapien ac ex tempus tempor. Cras odio augue, porta vitae venenatis id, sagittis at tortor. Etiam id tristique tortor, in eleifend dui. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.`;
```

If you reload your app, you should now be able to navigate to the news details screen.

![news details screen finished](https://i.imgur.com/K58tn15.png)

Note that you didn't have to add any additional configuration to use this last route. Given that we already setup the stack navigator for the "news" directory, every child automatically becomes a valid route ✨✨

## Conclusion

Congrats! If you are still here, you managed to build a small app that leverages a few of the available features on Expo Router.
This is just the beginning. We are in the early days of this library, so everything is changing really fast.
If you enjoyed this article, don't hesitate to leave a comment or reach out to me on Twitter. My DM's are always open.

I would love to hear your thoughts!
Happy coding!