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/c
just 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.