UNPKG

6.24 kBTypeScriptView Raw
1import {
2 getActionFromState as getActionFromStateDefault,
3 getStateFromPath as getStateFromPathDefault,
4 NavigationContainerRef,
5 ParamListBase,
6} from '@react-navigation/core';
7import * as React from 'react';
8import { Linking, Platform } from 'react-native';
9
10import extractPathFromURL from './extractPathFromURL';
11import type { LinkingOptions } from './types';
12
13type ResultState = ReturnType<typeof getStateFromPathDefault>;
14
15type Options = LinkingOptions<ParamListBase> & {
16 independent?: boolean;
17};
18
19let isUsingLinking = false;
20
21export default function useLinking(
22 ref: React.RefObject<NavigationContainerRef<ParamListBase>>,
23 {
24 independent,
25 enabled = true,
26 prefixes,
27 filter,
28 config,
29 getInitialURL = () =>
30 Promise.race([
31 Linking.getInitialURL(),
32 new Promise<undefined>((resolve) =>
33 // Timeout in 150ms if `getInitialState` doesn't resolve
34 // Workaround for https://github.com/facebook/react-native/issues/25675
35 setTimeout(resolve, 150)
36 ),
37 ]),
38 subscribe = (listener) => {
39 const callback = ({ url }: { url: string }) => listener(url);
40
41 const subscription = Linking.addEventListener('url', callback) as
42 | { remove(): void }
43 | undefined;
44
45 return () => {
46 // https://github.com/facebook/react-native/commit/6d1aca806cee86ad76de771ed3a1cc62982ebcd7
47 if (subscription?.remove) {
48 subscription.remove();
49 } else {
50 Linking.removeEventListener('url', callback);
51 }
52 };
53 },
54 getStateFromPath = getStateFromPathDefault,
55 getActionFromState = getActionFromStateDefault,
56 }: Options
57) {
58 React.useEffect(() => {
59 if (independent) {
60 return undefined;
61 }
62
63 if (enabled !== false && isUsingLinking) {
64 throw new Error(
65 [
66 'Looks like you have configured linking in multiple places. This is likely an error since deep links should only be handled in one place to avoid conflicts. Make sure that:',
67 "- You are not using both 'linking' prop and 'useLinking'",
68 "- You don't have 'useLinking' in multiple components",
69 Platform.OS === 'android'
70 ? "- You have set 'android:launchMode=singleTask' in the '<activity />' section of the 'AndroidManifest.xml' file to avoid launching multiple instances"
71 : '',
72 ]
73 .join('\n')
74 .trim()
75 );
76 } else {
77 isUsingLinking = enabled !== false;
78 }
79
80 return () => {
81 isUsingLinking = false;
82 };
83 });
84
85 // We store these options in ref to avoid re-creating getInitialState and re-subscribing listeners
86 // This lets user avoid wrapping the items in `React.useCallback` or `React.useMemo`
87 // Not re-creating `getInitialState` is important coz it makes it easier for the user to use in an effect
88 const enabledRef = React.useRef(enabled);
89 const prefixesRef = React.useRef(prefixes);
90 const filterRef = React.useRef(filter);
91 const configRef = React.useRef(config);
92 const getInitialURLRef = React.useRef(getInitialURL);
93 const getStateFromPathRef = React.useRef(getStateFromPath);
94 const getActionFromStateRef = React.useRef(getActionFromState);
95
96 React.useEffect(() => {
97 enabledRef.current = enabled;
98 prefixesRef.current = prefixes;
99 filterRef.current = filter;
100 configRef.current = config;
101 getInitialURLRef.current = getInitialURL;
102 getStateFromPathRef.current = getStateFromPath;
103 getActionFromStateRef.current = getActionFromState;
104 });
105
106 const getStateFromURL = React.useCallback(
107 (url: string | null | undefined) => {
108 if (!url || (filterRef.current && !filterRef.current(url))) {
109 return undefined;
110 }
111
112 const path = extractPathFromURL(prefixesRef.current, url);
113
114 return path
115 ? getStateFromPathRef.current(path, configRef.current)
116 : undefined;
117 },
118 []
119 );
120
121 const getInitialState = React.useCallback(() => {
122 let state: ResultState | undefined;
123
124 if (enabledRef.current) {
125 const url = getInitialURLRef.current();
126
127 if (url != null && typeof url !== 'string') {
128 return url.then((url) => {
129 const state = getStateFromURL(url);
130
131 return state;
132 });
133 }
134
135 state = getStateFromURL(url);
136 }
137
138 const thenable = {
139 then(onfulfilled?: (state: ResultState | undefined) => void) {
140 return Promise.resolve(onfulfilled ? onfulfilled(state) : state);
141 },
142 catch() {
143 return thenable;
144 },
145 };
146
147 return thenable as PromiseLike<ResultState | undefined>;
148 }, [getStateFromURL]);
149
150 React.useEffect(() => {
151 const listener = (url: string) => {
152 if (!enabled) {
153 return;
154 }
155
156 const navigation = ref.current;
157 const state = navigation ? getStateFromURL(url) : undefined;
158
159 if (navigation && state) {
160 // Make sure that the routes in the state exist in the root navigator
161 // Otherwise there's an error in the linking configuration
162 const rootState = navigation.getRootState();
163
164 if (state.routes.some((r) => !rootState?.routeNames.includes(r.name))) {
165 console.warn(
166 "The navigation state parsed from the URL contains routes not present in the root navigator. This usually means that the linking configuration doesn't match the navigation structure. See https://reactnavigation.org/docs/configuring-links for more details on how to specify a linking configuration."
167 );
168 return;
169 }
170
171 const action = getActionFromStateRef.current(state, configRef.current);
172
173 if (action !== undefined) {
174 try {
175 navigation.dispatch(action);
176 } catch (e) {
177 // Ignore any errors from deep linking.
178 // This could happen in case of malformed links, navigation object not being initialized etc.
179 console.warn(
180 `An error occurred when trying to handle the link '${url}': ${e.message}`
181 );
182 }
183 } else {
184 navigation.resetRoot(state);
185 }
186 }
187 };
188
189 return subscribe(listener);
190 }, [enabled, getStateFromURL, ref, subscribe]);
191
192 return {
193 getInitialState,
194 };
195}