React Native course Β· Module 3: Navigation

Deep Linking - Magic Portals

3 min read Β·
In this lesson8

Deep Linking is like magic portals in Atlantis - they allow users to jump directly to specific screens of the application from external links.

What is Deep Linking?

Deep Linking enables:

  • Opening the application from URL links
  • Navigating directly to a specific screen
  • Passing parameters through URL
  • Handling push notifications

Configuration in Expo

// app.json
{
  "expo": {
    "scheme": "atlantis",
    "web": {
      "bundler": "metro"
    }
  }
}

Configuring linking in NavigationContainer

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

// URL prefix for the application
const prefix = Linking.createURL('/');

type RootStackParamList = {
  Home: undefined;
  Profile: { userId: string };
  Item: { itemId: number };
  Settings: undefined;
};

const linking: LinkingOptions<RootStackParamList> = {
  prefixes: [prefix, 'atlantis://', 'https://atlantis.app'],

  config: {
    screens: {
      Home: '',
      Profile: 'user/:userId',
      Item: {
        path: 'item/:itemId',
        parse: {
          itemId: (itemId: string) => parseInt(itemId, 10),
        },
      },
      Settings: 'settings',
      // Nested navigators
      MainTabs: {
        screens: {
          Feed: 'feed',
          Messages: 'messages/:conversationId?',
        },
      },
    },
  },
};

function App() {
  return (
    <NavigationContainer
      linking={linking}
      fallback={<LoadingScreen />}
    >
      <Stack.Navigator>
        {/* Screens */}
      </Stack.Navigator>
    </NavigationContainer>
  );
}

Supported URL Formats

atlantis://                    β†’ Home
atlantis://user/123            β†’ Profile { userId: '123' }
atlantis://item/42             β†’ Item { itemId: 42 }
atlantis://settings            β†’ Settings
atlantis://feed                β†’ MainTabs > Feed
atlantis://messages/abc        β†’ MainTabs > Messages { conversationId: 'abc' }
import * as Linking from 'expo-linking';

function ShareButton({ itemId }: { itemId: number }) {
  const handleShare = async () => {
    // Internal link (to the app)
    const internalUrl = Linking.createURL(`item/${itemId}`);
    console.log(internalUrl); // exp://192.168.1.5:8081/--/item/42

    // Universal link (web + app)
    const universalUrl = `https://atlantis.app/item/${itemId}`;

    // Sharing
    await Share.share({
      message: `Check out this item: ${universalUrl}`,
      url: universalUrl,
    });
  };

  return (
    <Pressable onPress={handleShare}>
      <Text>Share</Text>
    </Pressable>
  );
}

Opening External URLs

import * as Linking from 'expo-linking';

function ExternalLinks() {
  const openWebsite = async () => {
    await Linking.openURL('https://atlantis.app');
  };

  const openSettings = async () => {
    // Opens the system settings for the app
    await Linking.openSettings();
  };

  const sendEmail = async () => {
    await Linking.openURL('mailto:contact@atlantis.app?subject=Question');
  };

  const makeCall = async () => {
    await Linking.openURL('tel:+48123456789');
  };

  const openMaps = async () => {
    // iOS
    await Linking.openURL('maps://app?daddr=52.2297,21.0122');
    // Android
    await Linking.openURL('geo:52.2297,21.0122');
  };

  return (
    <View>
      <Pressable onPress={openWebsite}><Text>Website</Text></Pressable>
      <Pressable onPress={openSettings}><Text>Settings</Text></Pressable>
      <Pressable onPress={sendEmail}><Text>Email</Text></Pressable>
      <Pressable onPress={makeCall}><Text>Call</Text></Pressable>
    </View>
  );
}
import * as Linking from 'expo-linking';
import { useEffect } from 'react';

function App() {
  useEffect(() => {
    // Check if the app was opened from a link
    const getInitialURL = async () => {
      const initialUrl = await Linking.getInitialURL();
      if (initialUrl) {
        console.log('Opened from link:', initialUrl);
      }
    };

    getInitialURL();

    // Listen for new links (when the app is in the background)
    const subscription = Linking.addEventListener('url', ({ url }) => {
      console.log('Received link:', url);
    });

    return () => subscription.remove();
  }, []);

  return <NavigationContainer linking={linking}>{/* ... */}</NavigationContainer>;
}

State Persistence (Preserving Navigation State)

import AsyncStorage from '@react-native-async-storage/async-storage';

const NAVIGATION_STATE_KEY = 'NAVIGATION_STATE';

function App() {
  const [isReady, setIsReady] = useState(false);
  const [initialState, setInitialState] = useState();

  useEffect(() => {
    const restoreState = async () => {
      try {
        const savedState = await AsyncStorage.getItem(NAVIGATION_STATE_KEY);
        if (savedState) {
          setInitialState(JSON.parse(savedState));
        }
      } finally {
        setIsReady(true);
      }
    };

    restoreState();
  }, []);

  if (!isReady) return <LoadingScreen />;

  return (
    <NavigationContainer
      initialState={initialState}
      onStateChange={(state) => {
        AsyncStorage.setItem(NAVIGATION_STATE_KEY, JSON.stringify(state));
      }}
    >
      {/* ... */}
    </NavigationContainer>
  );
}

Deep Linking significantly improves the UX of your application, enabling sharing of specific content and integration with push notifications.

Code for this lesson: App.tsx
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import * as Linking from 'expo-linking';

const Stack = createNativeStackNavigator();

// App URL prefix, e.g. atlantis://item/42
const prefix = Linking.createURL('/');

const linking = {
  prefixes: [prefix],
  // TODO: complete the config mapping paths to screens:
  // config: {
  //   screens: {
  //     Home: '',
  //     Details: 'item/:itemId',
  //   },
  // },
};

function HomeScreen() {
  return (
    <View style={styles.screen}>
      <Text style={styles.title}>Portal of Atlantis</Text>
    </View>
  );
}

function DetailsScreen({ route }) {
  return (
    <View style={styles.screen}>
      <Text style={styles.title}>Item #{route.params?.itemId}</Text>
    </View>
  );
}

export default function App() {
  return (
    // TODO: pass the linking={linking} prop to <NavigationContainer>
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Details" component={DetailsScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

const styles = StyleSheet.create({
  screen: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#1a5276',
  },
  title: {
    fontSize: 22,
    color: '#fff',
    fontWeight: '600',
  },
});

Docs entries used in this lesson

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What does the 'scheme' property in app.json define?

  2. 2. Which method opens an external URL or application?

Hands-on tasks in the game

  • Horizontal ordering

    Arrange creating a URL to share:

  • Code editor

    Complete the linking configuration.

Useful articles