UNPKG

6.92 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 linkingHandlers: Symbol[] = [];
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 // Storing this in a local variable stops Jest from complaining about import after teardown
46 const removeEventListener = Linking.removeEventListener?.bind(Linking);
47
48 return () => {
49 // https://github.com/facebook/react-native/commit/6d1aca806cee86ad76de771ed3a1cc62982ebcd7
50 if (subscription?.remove) {
51 subscription.remove();
52 } else {
53 removeEventListener?.('url', callback);
54 }
55 };
56 },
57 getStateFromPath = getStateFromPathDefault,
58 getActionFromState = getActionFromStateDefault,
59 }: Options
60) {
61 React.useEffect(() => {
62 if (process.env.NODE_ENV === 'production') {
63 return undefined;
64 }
65
66 if (independent) {
67 return undefined;
68 }
69
70 if (enabled !== false && linkingHandlers.length) {
71 console.error(
72 [
73 '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:',
74 "- You don't have multiple NavigationContainers in the app each with 'linking' enabled",
75 '- Only a single instance of the root component is rendered',
76 Platform.OS === 'android'
77 ? "- You have set 'android:launchMode=singleTask' in the '<activity />' section of the 'AndroidManifest.xml' file to avoid launching multiple instances"
78 : '',
79 ]
80 .join('\n')
81 .trim()
82 );
83 }
84
85 const handler = Symbol();
86
87 if (enabled !== false) {
88 linkingHandlers.push(handler);
89 }
90
91 return () => {
92 const index = linkingHandlers.indexOf(handler);
93
94 if (index > -1) {
95 linkingHandlers.splice(index, 1);
96 }
97 };
98 }, [enabled, independent]);
99
100 // We store these options in ref to avoid re-creating getInitialState and re-subscribing listeners
101 // This lets user avoid wrapping the items in `React.useCallback` or `React.useMemo`
102 // Not re-creating `getInitialState` is important coz it makes it easier for the user to use in an effect
103 const enabledRef = React.useRef(enabled);
104 const prefixesRef = React.useRef(prefixes);
105 const filterRef = React.useRef(filter);
106 const configRef = React.useRef(config);
107 const getInitialURLRef = React.useRef(getInitialURL);
108 const getStateFromPathRef = React.useRef(getStateFromPath);
109 const getActionFromStateRef = React.useRef(getActionFromState);
110
111 React.useEffect(() => {
112 enabledRef.current = enabled;
113 prefixesRef.current = prefixes;
114 filterRef.current = filter;
115 configRef.current = config;
116 getInitialURLRef.current = getInitialURL;
117 getStateFromPathRef.current = getStateFromPath;
118 getActionFromStateRef.current = getActionFromState;
119 });
120
121 const getStateFromURL = React.useCallback(
122 (url: string | null | undefined) => {
123 if (!url || (filterRef.current && !filterRef.current(url))) {
124 return undefined;
125 }
126
127 const path = extractPathFromURL(prefixesRef.current, url);
128
129 return path !== undefined
130 ? getStateFromPathRef.current(path, configRef.current)
131 : undefined;
132 },
133 []
134 );
135
136 const getInitialState = React.useCallback(() => {
137 let state: ResultState | undefined;
138
139 if (enabledRef.current) {
140 const url = getInitialURLRef.current();
141
142 if (url != null && typeof url !== 'string') {
143 return url.then((url) => {
144 const state = getStateFromURL(url);
145
146 return state;
147 });
148 }
149
150 state = getStateFromURL(url);
151 }
152
153 const thenable = {
154 then(onfulfilled?: (state: ResultState | undefined) => void) {
155 return Promise.resolve(onfulfilled ? onfulfilled(state) : state);
156 },
157 catch() {
158 return thenable;
159 },
160 };
161
162 return thenable as PromiseLike<ResultState | undefined>;
163 }, [getStateFromURL]);
164
165 React.useEffect(() => {
166 const listener = (url: string) => {
167 if (!enabled) {
168 return;
169 }
170
171 const navigation = ref.current;
172 const state = navigation ? getStateFromURL(url) : undefined;
173
174 if (navigation && state) {
175 // Make sure that the routes in the state exist in the root navigator
176 // Otherwise there's an error in the linking configuration
177 const rootState = navigation.getRootState();
178
179 if (state.routes.some((r) => !rootState?.routeNames.includes(r.name))) {
180 console.warn(
181 "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."
182 );
183 return;
184 }
185
186 const action = getActionFromStateRef.current(state, configRef.current);
187
188 if (action !== undefined) {
189 try {
190 navigation.dispatch(action);
191 } catch (e) {
192 // Ignore any errors from deep linking.
193 // This could happen in case of malformed links, navigation object not being initialized etc.
194 console.warn(
195 `An error occurred when trying to handle the link '${url}': ${
196 typeof e === 'object' && e != null && 'message' in e
197 ? // @ts-expect-error: we're already checking for this
198 e.message
199 : e
200 }`
201 );
202 }
203 } else {
204 navigation.resetRoot(state);
205 }
206 }
207 };
208
209 return subscribe(listener);
210 }, [enabled, getStateFromURL, ref, subscribe]);
211
212 return {
213 getInitialState,
214 };
215}