UNPKG

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