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:
- Navigates to the
Tabsnavigator - Selects the
HomeTabtab - Pushes the
Productscreen 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
Unit Testing Link Config
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.