UNPKG

13.4 kBJavaScriptView Raw
1import { findFocusedRoute, getActionFromState as getActionFromStateDefault, getPathFromState as getPathFromStateDefault, getStateFromPath as getStateFromPathDefault } from '@react-navigation/core';
2import isEqual from 'fast-deep-equal';
3import * as React from 'react';
4import createMemoryHistory from './createMemoryHistory';
5import ServerContext from './ServerContext';
6
7/**
8 * Find the matching navigation state that changed between 2 navigation states
9 * e.g.: a -> b -> c -> d and a -> b -> c -> e -> f, if history in b changed, b is the matching state
10 */
11const findMatchingState = (a, b) => {
12 if (a === undefined || b === undefined || a.key !== b.key) {
13 return [undefined, undefined];
14 } // Tab and drawer will have `history` property, but stack will have history in `routes`
15
16
17 const aHistoryLength = a.history ? a.history.length : a.routes.length;
18 const bHistoryLength = b.history ? b.history.length : b.routes.length;
19 const aRoute = a.routes[a.index];
20 const bRoute = b.routes[b.index];
21 const aChildState = aRoute.state;
22 const bChildState = bRoute.state; // Stop here if this is the state object that changed:
23 // - history length is different
24 // - focused routes are different
25 // - one of them doesn't have child state
26 // - child state keys are different
27
28 if (aHistoryLength !== bHistoryLength || aRoute.key !== bRoute.key || aChildState === undefined || bChildState === undefined || aChildState.key !== bChildState.key) {
29 return [a, b];
30 }
31
32 return findMatchingState(aChildState, bChildState);
33};
34/**
35 * Run async function in series as it's called.
36 */
37
38
39const series = cb => {
40 // Whether we're currently handling a callback
41 let handling = false;
42 let queue = [];
43
44 const callback = async () => {
45 try {
46 if (handling) {
47 // If we're currently handling a previous event, wait before handling this one
48 // Add the callback to the beginning of the queue
49 queue.unshift(callback);
50 return;
51 }
52
53 handling = true;
54 await cb();
55 } finally {
56 handling = false;
57
58 if (queue.length) {
59 // If we have queued items, handle the last one
60 const last = queue.pop();
61 last === null || last === void 0 ? void 0 : last();
62 }
63 }
64 };
65
66 return callback;
67};
68
69let linkingHandlers = [];
70export default function useLinking(ref, _ref) {
71 let {
72 independent,
73 enabled = true,
74 config,
75 getStateFromPath = getStateFromPathDefault,
76 getPathFromState = getPathFromStateDefault,
77 getActionFromState = getActionFromStateDefault
78 } = _ref;
79 React.useEffect(() => {
80 if (process.env.NODE_ENV === 'production') {
81 return undefined;
82 }
83
84 if (independent) {
85 return undefined;
86 }
87
88 if (enabled !== false && linkingHandlers.length) {
89 console.error(['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:', "- You don't have multiple NavigationContainers in the app each with 'linking' enabled", '- Only a single instance of the root component is rendered'].join('\n').trim());
90 }
91
92 const handler = Symbol();
93
94 if (enabled !== false) {
95 linkingHandlers.push(handler);
96 }
97
98 return () => {
99 const index = linkingHandlers.indexOf(handler);
100
101 if (index > -1) {
102 linkingHandlers.splice(index, 1);
103 }
104 };
105 }, [enabled, independent]);
106 const [history] = React.useState(createMemoryHistory); // We store these options in ref to avoid re-creating getInitialState and re-subscribing listeners
107 // This lets user avoid wrapping the items in `React.useCallback` or `React.useMemo`
108 // Not re-creating `getInitialState` is important coz it makes it easier for the user to use in an effect
109
110 const enabledRef = React.useRef(enabled);
111 const configRef = React.useRef(config);
112 const getStateFromPathRef = React.useRef(getStateFromPath);
113 const getPathFromStateRef = React.useRef(getPathFromState);
114 const getActionFromStateRef = React.useRef(getActionFromState);
115 React.useEffect(() => {
116 enabledRef.current = enabled;
117 configRef.current = config;
118 getStateFromPathRef.current = getStateFromPath;
119 getPathFromStateRef.current = getPathFromState;
120 getActionFromStateRef.current = getActionFromState;
121 });
122 const server = React.useContext(ServerContext);
123 const getInitialState = React.useCallback(() => {
124 let value;
125
126 if (enabledRef.current) {
127 var _server$location;
128
129 const location = (_server$location = server === null || server === void 0 ? void 0 : server.location) !== null && _server$location !== void 0 ? _server$location : typeof window !== 'undefined' ? window.location : undefined;
130 const path = location ? location.pathname + location.search : undefined;
131
132 if (path) {
133 value = getStateFromPathRef.current(path, configRef.current);
134 }
135 }
136
137 const thenable = {
138 then(onfulfilled) {
139 return Promise.resolve(onfulfilled ? onfulfilled(value) : value);
140 },
141
142 catch() {
143 return thenable;
144 }
145
146 };
147 return thenable; // eslint-disable-next-line react-hooks/exhaustive-deps
148 }, []);
149 const previousIndexRef = React.useRef(undefined);
150 const previousStateRef = React.useRef(undefined);
151 const pendingPopStatePathRef = React.useRef(undefined);
152 React.useEffect(() => {
153 previousIndexRef.current = history.index;
154 return history.listen(() => {
155 var _previousIndexRef$cur;
156
157 const navigation = ref.current;
158
159 if (!navigation || !enabled) {
160 return;
161 }
162
163 const path = location.pathname + location.search;
164 const index = history.index;
165 const previousIndex = (_previousIndexRef$cur = previousIndexRef.current) !== null && _previousIndexRef$cur !== void 0 ? _previousIndexRef$cur : 0;
166 previousIndexRef.current = index;
167 pendingPopStatePathRef.current = path; // When browser back/forward is clicked, we first need to check if state object for this index exists
168 // If it does we'll reset to that state object
169 // Otherwise, we'll handle it like a regular deep link
170
171 const record = history.get(index);
172
173 if ((record === null || record === void 0 ? void 0 : record.path) === path && record !== null && record !== void 0 && record.state) {
174 navigation.resetRoot(record.state);
175 return;
176 }
177
178 const state = getStateFromPathRef.current(path, configRef.current); // We should only dispatch an action when going forward
179 // Otherwise the action will likely add items to history, which would mess things up
180
181 if (state) {
182 // Make sure that the routes in the state exist in the root navigator
183 // Otherwise there's an error in the linking configuration
184 const rootState = navigation.getRootState();
185
186 if (state.routes.some(r => !(rootState !== null && rootState !== void 0 && rootState.routeNames.includes(r.name)))) {
187 console.warn("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.");
188 return;
189 }
190
191 if (index > previousIndex) {
192 const action = getActionFromStateRef.current(state, configRef.current);
193
194 if (action !== undefined) {
195 try {
196 navigation.dispatch(action);
197 } catch (e) {
198 // Ignore any errors from deep linking.
199 // This could happen in case of malformed links, navigation object not being initialized etc.
200 console.warn(`An error occurred when trying to handle the link '${path}': ${typeof e === 'object' && e != null && 'message' in e ? // @ts-expect-error: we're already checking for this
201 e.message : e}`);
202 }
203 } else {
204 navigation.resetRoot(state);
205 }
206 } else {
207 navigation.resetRoot(state);
208 }
209 } else {
210 // if current path didn't return any state, we should revert to initial state
211 navigation.resetRoot(state);
212 }
213 });
214 }, [enabled, history, ref]);
215 React.useEffect(() => {
216 var _ref$current;
217
218 if (!enabled) {
219 return;
220 }
221
222 const getPathForRoute = (route, state) => {
223 // If the `route` object contains a `path`, use that path as long as `route.name` and `params` still match
224 // This makes sure that we preserve the original URL for wildcard routes
225 if (route !== null && route !== void 0 && route.path) {
226 const stateForPath = getStateFromPathRef.current(route.path, configRef.current);
227
228 if (stateForPath) {
229 const focusedRoute = findFocusedRoute(stateForPath);
230
231 if (focusedRoute && focusedRoute.name === route.name && isEqual(focusedRoute.params, route.params)) {
232 return route.path;
233 }
234 }
235 }
236
237 return getPathFromStateRef.current(state, configRef.current);
238 };
239
240 if (ref.current) {
241 // We need to record the current metadata on the first render if they aren't set
242 // This will allow the initial state to be in the history entry
243 const state = ref.current.getRootState();
244
245 if (state) {
246 const route = findFocusedRoute(state);
247 const path = getPathForRoute(route, state);
248
249 if (previousStateRef.current === undefined) {
250 previousStateRef.current = state;
251 }
252
253 history.replace({
254 path,
255 state
256 });
257 }
258 }
259
260 const onStateChange = async () => {
261 const navigation = ref.current;
262
263 if (!navigation || !enabled) {
264 return;
265 }
266
267 const previousState = previousStateRef.current;
268 const state = navigation.getRootState(); // root state may not available, for example when root navigators switch inside the container
269
270 if (!state) {
271 return;
272 }
273
274 const pendingPath = pendingPopStatePathRef.current;
275 const route = findFocusedRoute(state);
276 const path = getPathForRoute(route, state);
277 previousStateRef.current = state;
278 pendingPopStatePathRef.current = undefined; // To detect the kind of state change, we need to:
279 // - Find the common focused navigation state in previous and current state
280 // - If only the route keys changed, compare history/routes.length to check if we go back/forward/replace
281 // - If no common focused navigation state found, it's a replace
282
283 const [previousFocusedState, focusedState] = findMatchingState(previousState, state);
284
285 if (previousFocusedState && focusedState && // We should only handle push/pop if path changed from what was in last `popstate`
286 // Otherwise it's likely a change triggered by `popstate`
287 path !== pendingPath) {
288 const historyDelta = (focusedState.history ? focusedState.history.length : focusedState.routes.length) - (previousFocusedState.history ? previousFocusedState.history.length : previousFocusedState.routes.length);
289
290 if (historyDelta > 0) {
291 // If history length is increased, we should pushState
292 // Note that path might not actually change here, for example, drawer open should pushState
293 history.push({
294 path,
295 state
296 });
297 } else if (historyDelta < 0) {
298 // If history length is decreased, i.e. entries were removed, we want to go back
299 const nextIndex = history.backIndex({
300 path
301 });
302 const currentIndex = history.index;
303
304 try {
305 if (nextIndex !== -1 && nextIndex < currentIndex) {
306 // An existing entry for this path exists and it's less than current index, go back to that
307 await history.go(nextIndex - currentIndex);
308 } else {
309 // We couldn't find an existing entry to go back to, so we'll go back by the delta
310 // This won't be correct if multiple routes were pushed in one go before
311 // Usually this shouldn't happen and this is a fallback for that
312 await history.go(historyDelta);
313 } // Store the updated state as well as fix the path if incorrect
314
315
316 history.replace({
317 path,
318 state
319 });
320 } catch (e) {// The navigation was interrupted
321 }
322 } else {
323 // If history length is unchanged, we want to replaceState
324 history.replace({
325 path,
326 state
327 });
328 }
329 } else {
330 // If no common navigation state was found, assume it's a replace
331 // This would happen if the user did a reset/conditionally changed navigators
332 history.replace({
333 path,
334 state
335 });
336 }
337 }; // We debounce onStateChange coz we don't want multiple state changes to be handled at one time
338 // This could happen since `history.go(n)` is asynchronous
339 // If `pushState` or `replaceState` were called before `history.go(n)` completes, it'll mess stuff up
340
341
342 return (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.addListener('state', series(onStateChange));
343 });
344 return {
345 getInitialState
346 };
347}
348//# sourceMappingURL=useLinking.js.map
\No newline at end of file