@react-navigation/core 6.2.2 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +17 -11
  2. package/lib/commonjs/PreventRemoveContext.js +17 -0
  3. package/lib/commonjs/PreventRemoveContext.js.map +1 -0
  4. package/lib/commonjs/PreventRemoveProvider.js +114 -0
  5. package/lib/commonjs/PreventRemoveProvider.js.map +1 -0
  6. package/lib/commonjs/getStateFromPath.js +1 -1
  7. package/lib/commonjs/getStateFromPath.js.map +1 -1
  8. package/lib/commonjs/index.js +36 -0
  9. package/lib/commonjs/index.js.map +1 -1
  10. package/lib/commonjs/types.js.map +1 -1
  11. package/lib/commonjs/useComponent.js +22 -12
  12. package/lib/commonjs/useComponent.js.map +1 -1
  13. package/lib/commonjs/useEventEmitter.js +1 -1
  14. package/lib/commonjs/useEventEmitter.js.map +1 -1
  15. package/lib/commonjs/useFocusEffect.js +1 -1
  16. package/lib/commonjs/useFocusEffect.js.map +1 -1
  17. package/lib/commonjs/useKeyedChildListeners.js +2 -2
  18. package/lib/commonjs/useKeyedChildListeners.js.map +1 -1
  19. package/lib/commonjs/useNavigationBuilder.js +4 -2
  20. package/lib/commonjs/useNavigationBuilder.js.map +1 -1
  21. package/lib/commonjs/usePreventRemove.js +59 -0
  22. package/lib/commonjs/usePreventRemove.js.map +1 -0
  23. package/lib/commonjs/usePreventRemoveContext.js +27 -0
  24. package/lib/commonjs/usePreventRemoveContext.js.map +1 -0
  25. package/lib/module/PreventRemoveContext.js +9 -0
  26. package/lib/module/PreventRemoveContext.js.map +1 -0
  27. package/lib/module/PreventRemoveProvider.js +96 -0
  28. package/lib/module/PreventRemoveProvider.js.map +1 -0
  29. package/lib/module/getStateFromPath.js +1 -1
  30. package/lib/module/getStateFromPath.js.map +1 -1
  31. package/lib/module/index.js +4 -0
  32. package/lib/module/index.js.map +1 -1
  33. package/lib/module/types.js.map +1 -1
  34. package/lib/module/useComponent.js +23 -12
  35. package/lib/module/useComponent.js.map +1 -1
  36. package/lib/module/useEventEmitter.js +1 -1
  37. package/lib/module/useEventEmitter.js.map +1 -1
  38. package/lib/module/useFocusEffect.js +1 -1
  39. package/lib/module/useFocusEffect.js.map +1 -1
  40. package/lib/module/useKeyedChildListeners.js +2 -2
  41. package/lib/module/useKeyedChildListeners.js.map +1 -1
  42. package/lib/module/useNavigationBuilder.js +3 -2
  43. package/lib/module/useNavigationBuilder.js.map +1 -1
  44. package/lib/module/usePreventRemove.js +41 -0
  45. package/lib/module/usePreventRemove.js.map +1 -0
  46. package/lib/module/usePreventRemoveContext.js +12 -0
  47. package/lib/module/usePreventRemoveContext.js.map +1 -0
  48. package/lib/typescript/src/PreventRemoveContext.d.ts +13 -0
  49. package/lib/typescript/src/PreventRemoveProvider.d.ts +9 -0
  50. package/lib/typescript/src/index.d.ts +4 -0
  51. package/lib/typescript/src/types.d.ts +10 -6
  52. package/lib/typescript/src/useComponent.d.ts +5 -1
  53. package/lib/typescript/src/useDescriptors.d.ts +3 -3
  54. package/lib/typescript/src/useNavigationBuilder.d.ts +9 -7
  55. package/lib/typescript/src/useNavigationHelpers.d.ts +3 -3
  56. package/lib/typescript/src/usePreventRemove.d.ts +12 -0
  57. package/lib/typescript/src/usePreventRemoveContext.d.ts +4 -0
  58. package/package.json +7 -5
  59. package/src/PreventRemoveContext.tsx +21 -0
  60. package/src/PreventRemoveProvider.tsx +126 -0
  61. package/src/getStateFromPath.tsx +4 -1
  62. package/src/index.tsx +4 -0
  63. package/src/types.tsx +39 -14
  64. package/src/useComponent.tsx +19 -12
  65. package/src/useEventEmitter.tsx +3 -1
  66. package/src/useFocusEffect.tsx +1 -1
  67. package/src/useKeyedChildListeners.tsx +6 -4
  68. package/src/useNavigationBuilder.tsx +6 -3
  69. package/src/usePreventRemove.tsx +51 -0
  70. package/src/usePreventRemoveContext.tsx +15 -0
@@ -0,0 +1,126 @@
1
+ import { nanoid } from 'nanoid/non-secure';
2
+ import * as React from 'react';
3
+ import useLatestCallback from 'use-latest-callback';
4
+
5
+ import NavigationHelpersContext from './NavigationHelpersContext';
6
+ import NavigationRouteContext from './NavigationRouteContext';
7
+ import PreventRemoveContext, { PreventedRoutes } from './PreventRemoveContext';
8
+
9
+ type Props = {
10
+ children: React.ReactNode;
11
+ };
12
+
13
+ type PreventedRoutesMap = Map<
14
+ string,
15
+ {
16
+ routeKey: string;
17
+ preventRemove: boolean;
18
+ }
19
+ >;
20
+
21
+ /**
22
+ * Util function to transform map of prevented routes to a simpler object.
23
+ */
24
+ const transformPreventedRoutes = (
25
+ preventedRoutesMap: PreventedRoutesMap
26
+ ): PreventedRoutes => {
27
+ const preventedRoutesToTransform = [...preventedRoutesMap.values()];
28
+
29
+ const preventedRoutes = preventedRoutesToTransform.reduce<PreventedRoutes>(
30
+ (acc, { routeKey, preventRemove }) => {
31
+ acc[routeKey] = {
32
+ preventRemove: acc[routeKey]?.preventRemove || preventRemove,
33
+ };
34
+ return acc;
35
+ },
36
+ {}
37
+ );
38
+
39
+ return preventedRoutes;
40
+ };
41
+
42
+ /**
43
+ * Component used for managing which routes have to be prevented from removal in native-stack.
44
+ */
45
+ export default function PreventRemoveProvider({ children }: Props) {
46
+ const [parentId] = React.useState(() => nanoid());
47
+ const [preventedRoutesMap, setPreventedRoutesMap] =
48
+ React.useState<PreventedRoutesMap>(new Map());
49
+
50
+ const navigation = React.useContext(NavigationHelpersContext);
51
+ const route = React.useContext(NavigationRouteContext);
52
+
53
+ const preventRemoveContextValue = React.useContext(PreventRemoveContext);
54
+ // take `setPreventRemove` from parent context - if exist it means we're in a nested context
55
+ const setParentPrevented = preventRemoveContextValue?.setPreventRemove;
56
+
57
+ const setPreventRemove = useLatestCallback(
58
+ (id: string, routeKey: string, preventRemove: boolean): void => {
59
+ if (
60
+ preventRemove &&
61
+ (navigation == null ||
62
+ navigation
63
+ ?.getState()
64
+ .routes.every((route) => route.key !== routeKey))
65
+ ) {
66
+ throw new Error(
67
+ `Couldn't find a route with the key ${routeKey}. Is your component inside NavigationContent?`
68
+ );
69
+ }
70
+
71
+ setPreventedRoutesMap((prevPrevented) => {
72
+ // values haven't changed - do nothing
73
+ if (
74
+ routeKey === prevPrevented.get(id)?.routeKey &&
75
+ preventRemove === prevPrevented.get(id)?.preventRemove
76
+ ) {
77
+ return prevPrevented;
78
+ }
79
+
80
+ const nextPrevented = new Map(prevPrevented);
81
+
82
+ if (preventRemove) {
83
+ nextPrevented.set(id, {
84
+ routeKey,
85
+ preventRemove,
86
+ });
87
+ } else {
88
+ nextPrevented.delete(id);
89
+ }
90
+
91
+ return nextPrevented;
92
+ });
93
+ }
94
+ );
95
+
96
+ const isPrevented = [...preventedRoutesMap.values()].some(
97
+ ({ preventRemove }) => preventRemove
98
+ );
99
+
100
+ React.useEffect(() => {
101
+ if (route?.key !== undefined && setParentPrevented !== undefined) {
102
+ // when route is defined (and setParentPrevented) it means we're in a nested stack
103
+ // route.key then will be the route key of parent
104
+ setParentPrevented(parentId, route.key, isPrevented);
105
+ return () => {
106
+ setParentPrevented(parentId, route.key, false);
107
+ };
108
+ }
109
+
110
+ return;
111
+ }, [parentId, isPrevented, route?.key, setParentPrevented]);
112
+
113
+ const value = React.useMemo(
114
+ () => ({
115
+ setPreventRemove,
116
+ preventedRoutes: transformPreventedRoutes(preventedRoutesMap),
117
+ }),
118
+ [setPreventRemove, preventedRoutesMap]
119
+ );
120
+
121
+ return (
122
+ <PreventRemoveContext.Provider value={value}>
123
+ {children}
124
+ </PreventRemoveContext.Provider>
125
+ );
126
+ }
@@ -565,7 +565,10 @@ const parseQueryParams = (
565
565
 
566
566
  if (parseConfig) {
567
567
  Object.keys(params).forEach((name) => {
568
- if (parseConfig[name] && typeof params[name] === 'string') {
568
+ if (
569
+ Object.hasOwnProperty.call(parseConfig, name) &&
570
+ typeof params[name] === 'string'
571
+ ) {
569
572
  params[name] = parseConfig[name](params[name] as string);
570
573
  }
571
574
  });
package/src/index.tsx CHANGED
@@ -11,6 +11,8 @@ export { default as NavigationContainerRefContext } from './NavigationContainerR
11
11
  export { default as NavigationContext } from './NavigationContext';
12
12
  export { default as NavigationHelpersContext } from './NavigationHelpersContext';
13
13
  export { default as NavigationRouteContext } from './NavigationRouteContext';
14
+ export { default as PreventRemoveContext } from './PreventRemoveContext';
15
+ export { default as PreventRemoveProvider } from './PreventRemoveProvider';
14
16
  export * from './types';
15
17
  export { default as useFocusEffect } from './useFocusEffect';
16
18
  export { default as useIsFocused } from './useIsFocused';
@@ -18,6 +20,8 @@ export { default as useNavigation } from './useNavigation';
18
20
  export { default as useNavigationBuilder } from './useNavigationBuilder';
19
21
  export { default as useNavigationContainerRef } from './useNavigationContainerRef';
20
22
  export { default as useNavigationState } from './useNavigationState';
23
+ export { default as UNSTABLE_usePreventRemove } from './usePreventRemove';
24
+ export { default as usePreventRemoveContext } from './usePreventRemoveContext';
21
25
  export { default as useRoute } from './useRoute';
22
26
  export { default as validatePathConfig } from './validatePathConfig';
23
27
  export * from '@react-navigation/routers';
package/src/types.tsx CHANGED
@@ -192,9 +192,18 @@ type NavigationHelpersCommon<
192
192
  * @param [params] Params object for the route.
193
193
  */
194
194
  navigate<RouteName extends keyof ParamList>(
195
- ...args: undefined extends ParamList[RouteName]
196
- ? [screen: RouteName] | [screen: RouteName, params: ParamList[RouteName]]
197
- : [screen: RouteName, params: ParamList[RouteName]]
195
+ ...args: // this first condition allows us to iterate over a union type
196
+ // This is to avoid getting a union of all the params from `ParamList[RouteName]`,
197
+ // which will get our types all mixed up if a union RouteName is passed in.
198
+ RouteName extends unknown
199
+ ? // This condition checks if the params are optional,
200
+ // which means it's either undefined or a union with undefined
201
+ undefined extends ParamList[RouteName]
202
+ ?
203
+ | [screen: RouteName] // if the params are optional, we don't have to provide it
204
+ | [screen: RouteName, params: ParamList[RouteName]]
205
+ : [screen: RouteName, params: ParamList[RouteName]]
206
+ : never
198
207
  ): void;
199
208
 
200
209
  /**
@@ -203,14 +212,16 @@ type NavigationHelpersCommon<
203
212
  * @param route Object with `key` or `name` for the route to navigate to, and a `params` object.
204
213
  */
205
214
  navigate<RouteName extends keyof ParamList>(
206
- options:
207
- | { key: string; params?: ParamList[RouteName]; merge?: boolean }
208
- | {
209
- name: RouteName;
210
- key?: string;
211
- params: ParamList[RouteName];
212
- merge?: boolean;
213
- }
215
+ options: RouteName extends unknown
216
+ ?
217
+ | { key: string; params?: ParamList[RouteName]; merge?: boolean }
218
+ | {
219
+ name: RouteName;
220
+ key?: string;
221
+ params: ParamList[RouteName];
222
+ merge?: boolean;
223
+ }
224
+ : never
214
225
  ): void;
215
226
 
216
227
  /**
@@ -325,7 +336,11 @@ export type NavigationProp<
325
336
  *
326
337
  * @param params Params object for the current route.
327
338
  */
328
- setParams(params: Partial<ParamList[RouteName]>): void;
339
+ setParams(
340
+ params: ParamList[RouteName] extends undefined
341
+ ? undefined
342
+ : Partial<ParamList[RouteName]>
343
+ ): void;
329
344
 
330
345
  /**
331
346
  * Update the options for the route.
@@ -436,6 +451,16 @@ export type ScreenListeners<
436
451
  >;
437
452
  }>;
438
453
 
454
+ type ScreenComponentType<
455
+ ParamList extends ParamListBase,
456
+ RouteName extends keyof ParamList
457
+ > =
458
+ | React.ComponentType<{
459
+ route: RouteProp<ParamList, RouteName>;
460
+ navigation: any;
461
+ }>
462
+ | React.ComponentType<{}>;
463
+
439
464
  export type RouteConfigComponent<
440
465
  ParamList extends ParamListBase,
441
466
  RouteName extends keyof ParamList
@@ -444,7 +469,7 @@ export type RouteConfigComponent<
444
469
  /**
445
470
  * React component to render for this screen.
446
471
  */
447
- component: React.ComponentType<any>;
472
+ component: ScreenComponentType<ParamList, RouteName>;
448
473
  getComponent?: never;
449
474
  children?: never;
450
475
  }
@@ -452,7 +477,7 @@ export type RouteConfigComponent<
452
477
  /**
453
478
  * Lazily get a React component to render for this screen.
454
479
  */
455
- getComponent: () => React.ComponentType<any>;
480
+ getComponent: () => ScreenComponentType<ParamList, RouteName>;
456
481
  component?: never;
457
482
  children?: never;
458
483
  }
@@ -1,30 +1,37 @@
1
1
  import * as React from 'react';
2
2
 
3
- export default function useComponent<
4
- T extends React.ComponentType<any>,
5
- P extends {}
6
- >(Component: T, props: P) {
7
- const propsRef = React.useRef<P | null>(props);
3
+ type Render = (children: React.ReactNode) => JSX.Element;
4
+
5
+ type Props = {
6
+ render: Render;
7
+ children: React.ReactNode;
8
+ };
9
+
10
+ const NavigationContent = ({ render, children }: Props) => {
11
+ return render(children);
12
+ };
13
+
14
+ export default function useComponent(render: Render) {
15
+ const renderRef = React.useRef<Render | null>(render);
8
16
 
9
17
  // Normally refs shouldn't be mutated in render
10
18
  // But we return a component which will be rendered
11
19
  // So it's just for immediate consumption
12
- propsRef.current = props;
20
+ renderRef.current = render;
13
21
 
14
22
  React.useEffect(() => {
15
- propsRef.current = null;
23
+ renderRef.current = null;
16
24
  });
17
25
 
18
- return React.useRef((rest: Omit<React.ComponentProps<T>, keyof P>) => {
19
- const props = propsRef.current;
26
+ return React.useRef(({ children }: { children: React.ReactNode }) => {
27
+ const render = renderRef.current;
20
28
 
21
- if (props === null) {
29
+ if (render === null) {
22
30
  throw new Error(
23
31
  'The returned component must be rendered in the same render phase as the hook.'
24
32
  );
25
33
  }
26
34
 
27
- // @ts-expect-error: the props should be fine here
28
- return <Component {...props} {...rest} />;
35
+ return <NavigationContent render={render}>{children}</NavigationContent>;
29
36
  }).current;
30
37
  }
@@ -21,7 +21,9 @@ export default function useEventEmitter<T extends Record<string, any>>(
21
21
  listenRef.current = listen;
22
22
  });
23
23
 
24
- const listeners = React.useRef<Record<string, Record<string, Listeners>>>({});
24
+ const listeners = React.useRef<Record<string, Record<string, Listeners>>>(
25
+ Object.create(null)
26
+ );
25
27
 
26
28
  const create = React.useCallback((target: string) => {
27
29
  const removeListener = (type: string, callback: (data: any) => void) => {
@@ -52,7 +52,7 @@ export default function useFocusEffect(effect: EffectCallback) {
52
52
  'Instead, write the async function inside your effect ' +
53
53
  'and call it immediately:\n\n' +
54
54
  'useFocusEffect(\n' +
55
- ' React.useCallback() => {\n' +
55
+ ' React.useCallback(() => {\n' +
56
56
  ' async function fetchData() {\n' +
57
57
  ' // You can await here\n' +
58
58
  ' const response = await MyAPI.getData(someId);\n' +
@@ -11,10 +11,12 @@ export default function useKeyedChildListeners() {
11
11
  string,
12
12
  KeyedListenerMap[K] | undefined
13
13
  >;
14
- }>({
15
- getState: {},
16
- beforeRemove: {},
17
- });
14
+ }>(
15
+ Object.assign(Object.create(null), {
16
+ getState: {},
17
+ beforeRemove: {},
18
+ })
19
+ );
18
20
 
19
21
  const addKeyedListener = React.useCallback(
20
22
  <T extends keyof KeyedListenerMap>(
@@ -19,6 +19,7 @@ import isRecordEqual from './isRecordEqual';
19
19
  import NavigationHelpersContext from './NavigationHelpersContext';
20
20
  import NavigationRouteContext from './NavigationRouteContext';
21
21
  import NavigationStateContext from './NavigationStateContext';
22
+ import PreventRemoveProvider from './PreventRemoveProvider';
22
23
  import Screen from './Screen';
23
24
  import {
24
25
  DefaultNavigatorOptions,
@@ -690,9 +691,11 @@ export default function useNavigationBuilder<
690
691
  descriptors,
691
692
  });
692
693
 
693
- const NavigationContent = useComponent(NavigationHelpersContext.Provider, {
694
- value: navigation,
695
- });
694
+ const NavigationContent = useComponent((children: React.ReactNode) => (
695
+ <NavigationHelpersContext.Provider value={navigation}>
696
+ <PreventRemoveProvider>{children}</PreventRemoveProvider>
697
+ </NavigationHelpersContext.Provider>
698
+ ));
696
699
 
697
700
  return {
698
701
  state,
@@ -0,0 +1,51 @@
1
+ import type { NavigationAction } from '@react-navigation/routers';
2
+ import { nanoid } from 'nanoid/non-secure';
3
+ import * as React from 'react';
4
+ import useLatestCallback from 'use-latest-callback';
5
+
6
+ import type { EventListenerCallback, EventMapCore } from './types';
7
+ import useNavigation from './useNavigation';
8
+ import usePreventRemoveContext from './usePreventRemoveContext';
9
+ import useRoute from './useRoute';
10
+
11
+ /**
12
+ * Hook to prevent screen from being removed. Can be used to prevent users from leaving the screen.
13
+ *
14
+ * @param preventRemove Boolean indicating whether to prevent screen from being removed.
15
+ * @param callback Function which is executed when screen was prevented from being removed.
16
+ */
17
+ export default function usePreventRemove(
18
+ preventRemove: boolean,
19
+ callback: (options: { data: { action: NavigationAction } }) => void
20
+ ) {
21
+ const [id] = React.useState(() => nanoid());
22
+
23
+ const navigation = useNavigation();
24
+ const { key: routeKey } = useRoute();
25
+
26
+ const { setPreventRemove } = usePreventRemoveContext();
27
+
28
+ React.useEffect(() => {
29
+ setPreventRemove(id, routeKey, preventRemove);
30
+ return () => {
31
+ setPreventRemove(id, routeKey, false);
32
+ };
33
+ }, [setPreventRemove, id, routeKey, preventRemove]);
34
+
35
+ const beforeRemoveListener = useLatestCallback<
36
+ EventListenerCallback<EventMapCore<any>, 'beforeRemove'>
37
+ >((e) => {
38
+ if (!preventRemove) {
39
+ return;
40
+ }
41
+
42
+ e.preventDefault();
43
+
44
+ callback({ data: e.data });
45
+ });
46
+
47
+ React.useEffect(
48
+ () => navigation?.addListener('beforeRemove', beforeRemoveListener),
49
+ [navigation, beforeRemoveListener]
50
+ );
51
+ }
@@ -0,0 +1,15 @@
1
+ import * as React from 'react';
2
+
3
+ import PreventRemoveContext from './PreventRemoveContext';
4
+
5
+ export default function usePreventRemoveContext() {
6
+ const value = React.useContext(PreventRemoveContext);
7
+
8
+ if (value == null) {
9
+ throw new Error(
10
+ "Couldn't find the prevent remove context. Is your component inside NavigationContent?"
11
+ );
12
+ }
13
+
14
+ return value;
15
+ }