Deep Links With Authentication in React Navigation

Authors
No items found.

Deep links enable you to link directly to a specific screen in your app from external sources, such as social media, emails, and notifications. They are great for improving user engagement and providing a seamless user experience.

Things get a little tricky if your app requires users to log in, though. If the user is not already logged in when opening the deep link, they will be directed to the login screen instead. To ensure a smooth user experience, after they successfully log in, it would be ideal to redirect them to the appropriate screen they originally intended to visit.

However, if you have set up deep links with React Navigation, you may have noticed that this is not handled out of the box. In this post, we'll go through a few approaches to implement this flow in older versions of React Navigation, and look at a new API that makes it much easier.

What we'll cover

In this post, we'll use React Navigation's static configuration API with automatic deep links for a simpler setup; however, the concepts can be applied to custom linking configurations as well.

For our example, let's say we have an app with the following screens:

  • Accessible without login:
    • SignIn
  • Accessible after login:
    • Home
    • Profile

What we'd like to achieve:

  • If the user opens a deep link to /profile while not logged in, they should be taken to the SignIn screen.
  • After successfully logging in, they should be redirected to the Profile screen.
  • After logging out and logging back in, if there was no deep link, they should be taken to the Home screen.

This should also work regardless of whether the app was started from a cold state (not running in the background) or was already running in the background.

You can see our desired behavior in the demo video:

In the demo, you can see that the deep link to the profile was not handled before login due to conditional screens, resulting in a warning. Still, it gets handled after login and shows the Profile screen.

Our navigator will be a stack navigator with two groups: authenticated and unauthenticated, which will conditionally render screens based on the user's authentication status using the if property.

const RootStack = createNativeStackNavigator({
  groups: {
    authenticated: {
      if: useIsSignedIn,
      screens: {
        Home: {
          screen: Home,
        },
        Profile: {
          screen: Profile,
        },
      },
    },
    unauthenticated: {
      if: () => !useIsSignedIn(),
      screens: {
        SignIn: {
          screen: SignIn,
        },
      },
    },
  },
});

Then, we use the navigator with automatic deep links enabled:

const Navigation = createStaticNavigation(RootStack);

export function App() {
  return (
    <Navigation
      linking={{
        enabled: "auto",
        prefixes: [createURL("/")],
      }}
    />
  );
}

If you are not familiar with automatic deep links, they automatically generate deep link paths based on screen names, e.g. ,profile to go to the Profile screen.  You can read more about it in React Navigation docs.

Manual approaches

In older versions of React Navigation, there is no built-in way to handle this flow, so we need to implement it manually. Here are two main approaches:

Approach 1: Remounting the navigation container

React Navigation always handles deep links on mount. So we can take advantage of this behavior to handle deep links after login by remounting the navigation container. This can be done by using a key prop on the Navigation component (or NavigationContainer in the dynamic API):

export function App() {
  const isSignedIn = useIsSignedIn();
  const lastDeepLink = useLastDeepLink();

  return (
    <Navigation
      key={isSignedIn ? "signed-in" : "signed-out"}
      linking={{
        enabled: "auto",
        prefixes: [createURL("/")],
        getInitialURL: () => lastDeepLink,
      }}
    />
  );
}

Here, we change the key prop based on the authentication status. This forces React to unmount and remount the navigation container when the user logs in or out.

The useLastDeepLink hook is a custom hook that keeps track of the last deep link URL opened, and clears it after the user logs in:

function useLastDeepLink() {
  const isSignedIn = useIsSignedIn();

  // Keep track of the incoming deep links to handle after logging in
  const [deepLink, setDeepLink] = useState(() =>
    isSignedIn ? null : Linking.getInitialURL()
  );

  useEffect(() => {
    // Clear initial URL after logging in
    // This prevents handling the same deep link again after logging out and logging back in
    if (isSignedIn) {
      setDeepLink(null);
    }

    // Store incoming deep links if not authenticated
    // They will be handled after logging in
    const subscription = Linking.addEventListener("url", ({ url }) => {
      if (!isSignedIn) {
        setDeepLink(url);
      }
    });

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

  return deepLink;
}

We need to manually track the last deep link and clear it after login, otherwise, it will lead to some issues:

  • Logging out and logging back would reuse initial deep link, so we need to clear it after first login.
  • If a deep link is opened while the user is already on login screen, it won't be handled after login unless we keep track of it and pass it to the navigation container, as React Navigation only handles the initial URL on mount.

Disadvantages:

  • There is no animation between screen transitions, and depending on the navigator, it may flicker.
  • The entire navigation state is reset, it's not possible to do it only in a nested navigator.
  • It needs manual tracking of deep links to avoid issues.

Another variant of this approach is to show a login screen first without rendering the navigation container at all. However, it still has the same disadvantages as above and doesn't work for more complex flows where you may want to navigate to other screens from the login screen (e.g., sign up, forgot password, etc.).

Approach 2: Manually navigating to the deep link after login

As an alternative, we can track incoming deep links and, when the user logs in, handle the deep link ourselves and navigate to the corresponding screen.

To keep track of incoming deep links, we can use the same useLastDeepLink hook used in the previous section. Then, we'd parse the deep link URL and navigate to the appropriate screen after login:

const linking = {
  enabled: 'auto',
  prefixes: [createURL('/')],
};

// Get the deep link configuration for our screens
// This is used by React Navigation to parse the incoming URLs internally
// But since we are handling deep links manually, we need this for parsing
const linkingPathConfig = createPathConfigForStaticNavigation(
  RootStack,
  // Pass `linking.options` if present
  undefined,
  true,
);

export function App() {
  const ref = useNavigationContainerRef();
  const authenticated = useAuth((state) => state.authenticated);

  const lastDeepLink = useLastDeepLink();

  // Handle the stored deep link after logging in
  useEffect(() => {
    const handleDeepLink = async () => {
      const url =
        typeof lastDeepLink === 'string' ? lastDeepLink : await lastDeepLink;

      if (ref.isReady() && url && linkingPathConfig) {
        // Strip the prefix from the URL
        const path = linking.prefixes.reduce((acc, prefix) => {
          if (acc.startsWith(prefix)) {
            return acc.slice(prefix.length);
          }

          return acc;
        }, url);

        // Get the navigation state from the path
        const state = getStateFromPath(path, { screens: linkingPathConfig });

        // If we have a valid state, get the action and dispatch it
        // This will navigate to the intended screen
        if (state) {
          const action = getActionFromState(state);

          ref.dispatch(action!);
        }
      }
    };

    if (authenticated && lastDeepLink) {
      handleDeepLink();
    }
  }, [authenticated, ref]);

  return <Navigation ref={ref} linking={linking} />;
}

Here, we check if there is a stored deep link after the user logs in. If there is, we parse the URL to get the navigation state, then derive the navigation action from that state and dispatch it to navigate to the intended screen.

After stripping the prefix from the deep link URL, we parse it with getStateFromPath from @react-navigation/native to match how React Navigation parses it. It takes our linking options as an argument, which we can also generate with createPathConfigForStaticNavigation when using the static API.

This is similar to how React Navigation handles deep links internally, but we are doing it manually after login.

Disadvantages:

  • Requires more manual work and things to keep track of.
  • There will be a separate navigation after login, which may not feel as smooth.
  • The initial route will always be in the stack, which may not be desirable in some cases.

Built-in API in React Navigation 7

React Navigation 7 adds a new API to handle this use case more seamlessly. If you are on the latest version of all @react-navigation packages, you can enable it by setting UNSTABLE_routeNamesChangeBehavior to 'lastUnhandled' in the navigator that handles authentication state changes:

const RootStack = createNativeStackNavigator({
  UNSTABLE_routeNamesChangeBehavior: 'lastUnhandled',

  // ... rest of the navigator config
});

To understand what this does, let's review what happens when the authentication state changes:

  • When the user logs in, the list of available screens (names of routes) changes based on the new authentication state.
  • If none of the previously rendered screens are available anymore, React Navigation shows the first screen in the new list of available screens.

The UNSTABLE_routeNamesChangeBehavior option essentially lets us customize this behavior. By default, it's set to firstMatch, which means it will show the first screen in the new list of available screens.

By setting it to lastUnhandled, we tell React Navigation to remember any unhandled actions (like deep links) that were attempted before the authentication state changed. So when the user logs in, if there was a deep link that couldn't be handled before, React Navigation will retry handling that deep link after the authentication state changes, navigating the user to the intended screen.

This approach has several advantages:

  • It's built-in and requires minimal code, we only need to set a single option in the navigator.
  • The navigation is seamless, with proper animations between screens.
  • It works well with nested navigators, only the navigator where the authentication state changes needs to have this option set.

However, as the name suggests, this API is marked as unstable, which means it may change in future releases depending on user feedback and further testing. So it is the perfect time to try it out and provide feedback. You can post your thoughts and feedback in the React Navigation GitHub discussions.

Demo app

You can find a demo app implementing the approaches discussed in this post in this GitHub repository.

The repository has three branches, each implementing a different approach:

The app can be run with Expo Go.

To test the deep links, you can use the uri-scheme utility by Expo, for example:

npx uri-scheme open 'exp://192.168.0.31:8081/--/profile' --ios

In this case, the exp://192.168.0.31:8081 prefix refers to the server URL of the Expo Go app running on your machine, and the deep link path is /profile.

Conclusion

These approaches show how handling deep links is challenging when authentication is involved, with many gotchas and complexity when trying to handle them manually. I tried to solve these problems with the new UNSTABLE_routeNamesChangeBehavior option in React Navigation 7, and tried to keep it simple to use without the need for complex code and workarounds.

If your app relies on deep links, be sure to give this a try, and don’t forget to provide feedback.

Table of contents
Need help with React or React Native projects?

We support teams building scalable apps with React and React Native.

Let’s chat

//

//
Insights

Learn more about React Native

Here's everything we published recently on this topic.

//
React Native

We can help you move
it forward!

At Callstack, we work with companies big and small, pushing React Native everyday.

React Native Performance Optimization

Improve React Native apps speed and efficiency through targeted performance enhancements.

Code Sharing

Implement effective code-sharing strategies across all platforms to accelerate shipping and reduce code duplication.

Mobile App Development

Launch on both Android and iOS with single codebase, keeping high-performance and platform-specific UX.

React Native Development

Hire expert React Native engineers to build, scale, or improve your app, from day one to production.