expo: useURL always returns null as a first value even if the app is launched with a deep linking URL

Minimal reproducible example

https://snack.expo.dev/@andrew-tevent/useurl-first-value

Summary

Note: The above snack could just be used to capture the first URL, but I don’t know how/if you can pass a URL to a snack.

It seems there is an issue with Expo Linking - specifically the useURL hook: https://github.com/expo/expo/blob/main/packages/expo-linking/src/Linking.ts#L358

The react native docs state of getInitialURL that “If the app launch was triggered by an app link, it will give the link url, otherwise it will give null.”

But the useURL hook always returns null on the first execution, which doesn’t seem right as it means you can’t make a correct decision until there has been at least one update to the url value returned.

The issue being that getInitialURL is async.

It might be preferable for url to be string | null | undefined - with undefined for the initial value until the first getInitialURL result has been assigned?

Environment

expo-env-info 1.0.5 environment info: System: OS: Windows 10 10.0.22621 Binaries: Node: 18.13.0 - C:\Program Files\nodejs\node.EXE Yarn: 1.22.19 - ~\AppData\Roaming\npm\yarn.CMD npm: 8.19.3 - C:\Program Files\nodejs\npm.CMD IDEs: Android Studio: AI-213.7172.25.2113.9123335 npmPackages: expo: ^48.0.19 => 48.0.19 react: 18.2.0 => 18.2.0 react-native: 0.71.8 => 0.71.8 Expo Workflow: managed

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 2
  • Comments: 18 (1 by maintainers)

Most upvoted comments

My workaround is, for now:

import { getInitialURL, useURL } from "expo-linking";
import { useState, useEffect } from "react";

export const useBetterURL = (): string | null | undefined => {
  const url = useURL();
  const [urlState, setUrlState] = useState<string | null | undefined>(undefined);

  useEffect(() => {
    async function updateURL() {
      if (urlState === undefined) {
        // It seems like url is always null from the useURL (possibly because of the async nature of getInitialURL) until we explicitly call getInitialUrl.
        // So therefore, first time the URL gets a value from useURL, we call getInitialURL ourselves to get the first value.
        // See https://github.com/expo/expo/issues/23333
        const initialUrl = await getInitialURL();
        setUrlState(initialUrl);
        return;
      }

      if (url === urlState) {
        return;
      }

      setUrlState(url);
    }

    void updateURL();
  }, [url, urlState]);

  return urlState;
};

This introduces an undefined value for the URL until it is initially set - which will take place shortly after the call to getInitialURL().

Facing the same issue. For some reason I just ended up using the React Context API which seemed to mitigate the issue most of the time

Using SDK 50

apparently this is happen for me when the app is already in the foreground/background but the screen where i call useURL hasn’t been mounted, and will only be mounted via deeplinking.

so i ended up solving this by creating a context

URLProvider.tsx

import React, { createContext, useContext } from 'react';
import { useURL as useURLHook } from 'expo-linking';

const UrlContext = createContext<string | null>(null);

export const useURL = () => {
  return useContext(UrlContext);
};
export const UrlProvider = ({ children }: React.PropsWithChildren) => {
  const url = useURLHook();

  return <UrlContext.Provider value={url}>{children}</UrlContext.Provider>;
};

Every where or any screen in the app

import { useURL } from 'path/to/provider'

const Screen = () => {
    const url =  useURL() ?? ""
    return <Text>{Text}</Text>
}

now this is working as expected, Linking.getInitialUrl() works properly and Linking.addEventListener("url") gets called properly as well

solution here I’m using for deeplink magic links, it might not suit all use cases but the above did not work for me (for both when the app was in a closed state and in the foreground).

const validateLink = (url: string | null | undefined): boolean => {
  // logic to validate the url.
}
export const useBetterURL = (): string | null | undefined => {
  const nav = useNavigation()
  const navState = nav.getState()
  const routeIndex = navState.index
  const route = navState.routes[routeIndex]

  const path = route?.path
  const [urlState, setUrlState] = useState<string | null | undefined>(undefined)

  useEffect(() => {
    if (validateLink(path)) {
      setUrlState(`${env.APP_DEEPLINK_SCHEME}://${path}`)
    }
  }, [path])

  return urlState
}

Any update on this?

I tried the temporary workaround and it still returns null. For us, getInitialURL returns null when we open the app via a scheme deeplink (myapp://some/path)

Have not tried yet with a https deeplink as we can’t set it at the moment, but will update once I’m able to spend some time doing it in the next few days.