DEV Community

Cover image for Deep Linking in React Native Was a Mess; This Package Fixes It
Oluwaseyi Komolafe
Oluwaseyi Komolafe

Posted on

Deep Linking in React Native Was a Mess; This Package Fixes It

Deep Linking in React Native Was a Mess — So I Built a Package to Fix It

Deep linking is one of those things every React Native app eventually needs… and almost nobody enjoys implementing.

On paper, it’s simple:

Open a link like myapp://profile/123 → take the user to their profile screen.

In reality, it quickly turns into a mess.

  • The app might be closed (cold start)
  • Or already open (warm start)
  • You might need to support universal links (https://myapp.com/...)
  • The user might not be authenticated yet
  • You end up hardcoding URLs all over your codebase

And suddenly, something that should take an hour turns into a fragile system full of edge cases.

Most existing solutions don’t help much:

  • Some are outdated
  • Some are tightly coupled to a specific navigator
  • None handle the full lifecycle of deep linking cleanly

So I built one:

👉 react-native-deeplink-manager


What It Solves

This package is built around one idea:

Deep linking shouldn’t depend on your navigation library.

It’s completely navigator-agnostic and handles everything from receiving a link → validating it → navigating → tracking.

Core features

  • Supports custom schemes (myapp://) and universal/app links (https://)
  • Pattern-based routing with params (/user/:id)
  • Handles cold start + warm start automatically
  • Built-in auth guards (beforeNavigate)
  • Deferred deep links (store and process later)
  • Works with:

    • React Navigation
    • Expo Router
    • Custom navigators
  • Type-safe link builder

  • Full TypeScript support


Installation

npm install react-native-deeplink-manager
# or
yarn add react-native-deeplink-manager
Enter fullscreen mode Exit fullscreen mode

Quick Setup (React Navigation)

import { useEffect } from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import {
  createDeepLinkHandler,
  createReactNavigationAdapter,
  createNavigationRef,
} from 'react-native-deeplink-manager';

const Stack = createNativeStackNavigator();
const navigationRef = createNavigationRef();

export default function App() {
  useEffect(() => {
    const handler = createDeepLinkHandler({
      config: {
        schemes: ['myapp'],
        prefixes: ['https://myapp.com'],
        routes: [
          { path: '/home', name: 'Home' },
          { path: '/user/:id', name: 'UserProfile' },
        ],
      },
    });

    const adapter = createReactNavigationAdapter(navigationRef);
    handler.initialize(adapter);

    return () => handler.destroy();
  }, []);

  return (
    <NavigationContainer ref={navigationRef}>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="UserProfile" component={UserProfileScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}
Enter fullscreen mode Exit fullscreen mode

That’s it.

No manual parsing. No edge-case juggling. It just works.


Route Matching Without Pain

Define your routes once:

{ path: '/user/:id', name: 'UserProfile' }
Enter fullscreen mode Exit fullscreen mode

Now this:

myapp://myapp.com/user/123?ref=notification
Enter fullscreen mode Exit fullscreen mode

Automatically becomes:

params = { id: '123' }
queryParams = { ref: 'notification' }
Enter fullscreen mode Exit fullscreen mode

No regex. No custom parsing logic.


Auth Guards (Before Navigation Happens)

{
  path: '/dashboard',
  name: 'Dashboard',
  beforeNavigate: async () => {
    const isAuthenticated = await checkAuthStatus();

    if (!isAuthenticated) {
      navigateToLogin();
      return false;
    }

    return true;
  },
}
Enter fullscreen mode Exit fullscreen mode

Block navigation before it happens. Redirect if needed. No hacks.


Stop Hardcoding URLs

Build links programmatically:

import { createDeepLinkBuilder } from 'react-native-deeplink-manager';

const builder = createDeepLinkBuilder('myapp', 'myapp.com');

builder.build('/user/:id', { id: '123' });
// → myapp://myapp.com/user/123

builder.build('/search', undefined, { q: 'react native' });
// → myapp://myapp.com/search?q=react%20native
Enter fullscreen mode Exit fullscreen mode

This keeps your links consistent and type-safe.


Deferred Deep Links

Sometimes the app isn’t ready yet — maybe:

  • user isn’t logged in
  • navigation isn’t mounted

Store the link and process it later:

handler.setPendingLink('myapp://myapp.com/dashboard');

// later...
const link = handler.getPendingLink();
if (link) {
  await handler.handleLink(link);
  handler.clearPendingLink();
}
Enter fullscreen mode Exit fullscreen mode

Analytics Hooks

Track everything:

const handler = createDeepLinkHandler({
  config: {
    schemes: ['myapp'],
    routes: [...],
    onLinkNotMatched: (url) => {
      analytics.track('deep_link_not_matched', { url });
    },
  },
  onColdStart: (url, match) => {
    analytics.track('app_opened_from_link', { url });
  },
  onWarmStart: (url, match) => {
    analytics.track('in_app_deep_link', { url });
  },
});
Enter fullscreen mode Exit fullscreen mode

Works Beyond React Navigation

Using Expo Router?

const adapter = createExpoRouterAdapter(router);
handler.initialize(adapter);
Enter fullscreen mode Exit fullscreen mode

Using something custom?

const myAdapter = async (match) => {
  MyNavigator.navigate(match.route.name, {
    ...match.params,
    ...match.queryParams,
  });
};

handler.initialize(myAdapter);
Enter fullscreen mode Exit fullscreen mode

Why Not Just Use React Navigation Linking?

React Navigation’s linking prop is useful—but limited:

  • Locked into React Navigation
  • No auth guards
  • No link builder
  • No deferred links
  • No lifecycle hooks

This package focuses on the entire deep linking lifecycle, not just navigation.


Links


Final Thought

Deep linking shouldn’t feel like stitching together edge cases.

It should feel like routing.

That’s the gap this package tries to close.

Top comments (0)