Skip to content
Tolinku
Tolinku
Sign In Start Free
Engineering · · 5 min read

React Navigation Deep Links: Configuration Guide

By Tolinku Staff
|
Tolinku webhooks integrations dashboard screenshot for engineering blog posts

React Navigation is the standard navigation library for React Native. Its deep linking integration maps URLs directly to your navigation state, so a URL like /product/123 opens the Product screen with id: "123" as a parameter. This guide covers configuration for React Navigation v6 and v7.

For the native platform setup (AppDelegate, AndroidManifest), see React Native Deep Linking: Complete Setup Tutorial. For a cross-platform overview, see Cross-Platform Deep Linking Guide.

Basic Linking Configuration

React Navigation's linking prop on NavigationContainer connects URLs to screens.

Define the Linking Config

import * as Linking from 'expo-linking';

const linking = {
  prefixes: [
    'https://go.yourapp.com',       // Universal Links / App Links
    'yourapp://',                     // Custom URL scheme
  ],
  config: {
    screens: {
      Home: '',
      Product: 'product/:id',
      Category: 'category/:slug',
      Profile: 'profile/:username',
      Settings: 'settings',
      NotFound: '*',
    },
  },
};

Apply to NavigationContainer

function App() {
  return (
    <NavigationContainer linking={linking} fallback={<LoadingScreen />}>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Product" component={ProductScreen} />
        <Stack.Screen name="Category" component={CategoryScreen} />
        <Stack.Screen name="Profile" component={ProfileScreen} />
        <Stack.Screen name="Settings" component={SettingsScreen} />
        <Stack.Screen name="NotFound" component={NotFoundScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

The fallback prop shows a loading screen while React Navigation resolves the initial deep link. Without it, users may briefly see the home screen before being redirected.

Access Parameters in Screens

function ProductScreen({ route }) {
  const { id } = route.params;

  return <Text>Product ID: {id}</Text>;
}

Nested Navigators

Real apps have nested navigators: a tab navigator containing stack navigators. Deep linking configuration must mirror this nesting.

Example: Tabs with Nested Stacks

// Navigation structure:
// TabNavigator
//   ├── HomeTab (Stack)
//   │     ├── Home
//   │     └── Product
//   ├── SearchTab (Stack)
//   │     ├── Search
//   │     └── SearchResults
//   └── ProfileTab (Stack)
//         ├── Profile
//         └── Settings

const linking = {
  prefixes: ['https://go.yourapp.com', 'yourapp://'],
  config: {
    screens: {
      Tabs: {
        screens: {
          HomeTab: {
            screens: {
              Home: '',
              Product: 'product/:id',
            },
          },
          SearchTab: {
            screens: {
              Search: 'search',
              SearchResults: 'search/results',
            },
          },
          ProfileTab: {
            screens: {
              Profile: 'profile/:username',
              Settings: 'settings',
            },
          },
        },
      },
      NotFound: '*',
    },
  },
};

When a user opens https://go.yourapp.com/product/123, React Navigation:

  1. Navigates to the Tabs navigator
  2. Selects the HomeTab tab
  3. Pushes the Product screen with { id: "123" }

The user sees the Product screen with the HomeTab selected in the tab bar.

Query Parameters

Handle query parameters alongside path parameters:

const linking = {
  config: {
    screens: {
      Search: {
        path: 'search',
        parse: {
          query: (query) => query || '',
          sort: (sort) => sort || 'relevance',
        },
      },
    },
  },
};

URL: https://go.yourapp.com/search?query=shoes&sort=price

function SearchScreen({ route }) {
  const { query, sort } = route.params;
  // query = "shoes", sort = "price"
}

Custom Parameter Parsing

By default, path parameters are strings. Use parse and stringify to transform them:

const linking = {
  config: {
    screens: {
      Product: {
        path: 'product/:id',
        parse: {
          id: Number,  // Convert string "123" to number 123
        },
        stringify: {
          id: (id) => String(id),  // Convert back for URL generation
        },
      },
      DateFilter: {
        path: 'events/:date',
        parse: {
          date: (date) => new Date(date),
        },
        stringify: {
          date: (date) => date.toISOString().split('T')[0],
        },
      },
    },
  },
};

Handling Authentication Guards

A common scenario: a deep link points to a screen that requires authentication. The user taps the link but isn't logged in.

Approach 1: Redirect After Login

Store the intended deep link and redirect after authentication:

function App() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const [pendingDeepLink, setPendingDeepLink] = useState(null);

  const linking = {
    prefixes: ['https://go.yourapp.com'],
    config: {
      screens: isLoggedIn
        ? {
            Home: '',
            Product: 'product/:id',
            Profile: 'profile/:username',
          }
        : {
            Login: '*',  // All links go to Login when not authenticated
          },
    },
    getStateFromPath: (path, options) => {
      if (isLoggedIn === false) {
        // Save the path for after login
        setPendingDeepLink(path);
      }
      return undefined; // Use default behavior
    },
  };

  // After login, navigate to the saved deep link
  useEffect(() => {
    if (isLoggedIn && pendingDeepLink) {
      navigation.navigate(pendingDeepLink);
      setPendingDeepLink(null);
    }
  }, [isLoggedIn]);

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

Approach 2: Conditional Screens

Use React Navigation's conditional rendering:

<Stack.Navigator>
  {isLoggedIn ? (
    <>
      <Stack.Screen name="Home" component={HomeScreen} />
      <Stack.Screen name="Product" component={ProductScreen} />
      <Stack.Screen name="Profile" component={ProfileScreen} />
    </>
  ) : (
    <>
      <Stack.Screen name="Login" component={LoginScreen} />
      <Stack.Screen name="Register" component={RegisterScreen} />
    </>
  )}
</Stack.Navigator>

React Navigation handles this gracefully: if a deep link targets a screen that doesn't exist in the current navigator state, it falls through to the wildcard or default screen.

Advanced: Custom getStateFromPath

For complex routing logic that the declarative config can't handle, override getStateFromPath:

import { getStateFromPath as defaultGetStateFromPath } from '@react-navigation/native';

const linking = {
  prefixes: ['https://go.yourapp.com'],
  config: {
    screens: {
      Home: '',
      Product: 'product/:id',
    },
  },
  getStateFromPath: (path, options) => {
    // Custom logic: redirect legacy paths
    if (path.startsWith('/item/')) {
      path = path.replace('/item/', '/product/');
    }

    // Custom logic: extract campaign tracking
    const url = new URL('https://go.yourapp.com' + path);
    const utm_source = url.searchParams.get('utm_source');
    if (utm_source) {
      trackCampaignOpen(utm_source);
    }

    // Fall back to default parsing
    return defaultGetStateFromPath(path, options);
  },
};

This is useful for:

  • Redirecting old URL patterns to new ones
  • Extracting tracking parameters before navigation
  • Adding analytics events on deep link entry
  • Handling A/B test routing

Debugging

Enable Debug Logging

React Navigation has built-in deep link debugging. Add the onStateChange callback:

<NavigationContainer
  linking={linking}
  onStateChange={(state) => {
    console.log('Navigation state:', JSON.stringify(state, null, 2));
  }}
>

Common Debug Scenarios

Link opens the wrong screen: Check that your path pattern matches the URL. Remember that paths are matched in order, and the first match wins. A wildcard * at the top of your config would match everything.

Parameters are undefined: Verify the parameter name in your path pattern matches what you access in route.params. product/:id gives you route.params.id, not route.params.productId.

Nested screen not reached: Verify the nesting in your linking config matches your navigator nesting exactly. If the navigator structure is Tabs > HomeStack > Product, your config must be { Tabs: { screens: { HomeStack: { screens: { Product: 'product/:id' } } } } }.

Testing

Test your linking configuration without running the full app:

import { getStateFromPath } from '@react-navigation/native';

test('product deep link resolves correctly', () => {
  const state = getStateFromPath('/product/123', linking.config);
  expect(state.routes[0].name).toBe('Product');
  expect(state.routes[0].params.id).toBe('123');
});

test('unknown path resolves to NotFound', () => {
  const state = getStateFromPath('/unknown/path', linking.config);
  expect(state.routes[0].name).toBe('NotFound');
});

Integration Testing

Use React Navigation's testing utilities to test deep link handling end-to-end in your test suite.

For the Tolinku SDK integration, see the React Native SDK docs. For routing patterns, see Deep Link Routing Guide.

Get deep linking tips in your inbox

One email per week. No spam.

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.