UNPKG

23.2 kBJavaScriptView Raw
1"use strict";
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.default = useNavigationBuilder;
7
8var _routers = require("@react-navigation/routers");
9
10var React = _interopRequireWildcard(require("react"));
11
12var _reactIs = require("react-is");
13
14var _Group = _interopRequireDefault(require("./Group"));
15
16var _isArrayEqual = _interopRequireDefault(require("./isArrayEqual"));
17
18var _isRecordEqual = _interopRequireDefault(require("./isRecordEqual"));
19
20var _NavigationHelpersContext = _interopRequireDefault(require("./NavigationHelpersContext"));
21
22var _NavigationRouteContext = _interopRequireDefault(require("./NavigationRouteContext"));
23
24var _NavigationStateContext = _interopRequireDefault(require("./NavigationStateContext"));
25
26var _PreventRemoveProvider = _interopRequireDefault(require("./PreventRemoveProvider"));
27
28var _Screen = _interopRequireDefault(require("./Screen"));
29
30var _types = require("./types");
31
32var _useChildListeners = _interopRequireDefault(require("./useChildListeners"));
33
34var _useComponent = _interopRequireDefault(require("./useComponent"));
35
36var _useCurrentRender = _interopRequireDefault(require("./useCurrentRender"));
37
38var _useDescriptors = _interopRequireDefault(require("./useDescriptors"));
39
40var _useEventEmitter = _interopRequireDefault(require("./useEventEmitter"));
41
42var _useFocusedListenersChildrenAdapter = _interopRequireDefault(require("./useFocusedListenersChildrenAdapter"));
43
44var _useFocusEvents = _interopRequireDefault(require("./useFocusEvents"));
45
46var _useKeyedChildListeners = _interopRequireDefault(require("./useKeyedChildListeners"));
47
48var _useNavigationHelpers = _interopRequireDefault(require("./useNavigationHelpers"));
49
50var _useOnAction = _interopRequireDefault(require("./useOnAction"));
51
52var _useOnGetState = _interopRequireDefault(require("./useOnGetState"));
53
54var _useOnRouteFocus = _interopRequireDefault(require("./useOnRouteFocus"));
55
56var _useRegisterNavigator = _interopRequireDefault(require("./useRegisterNavigator"));
57
58var _useScheduleUpdate = _interopRequireDefault(require("./useScheduleUpdate"));
59
60function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
61
62function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
63
64function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
65
66// This is to make TypeScript compiler happy
67// eslint-disable-next-line babel/no-unused-expressions
68_types.PrivateValueStore;
69
70const isValidKey = key => key === undefined || typeof key === 'string' && key !== '';
71/**
72 * Extract route config object from React children elements.
73 *
74 * @param children React Elements to extract the config from.
75 */
76
77
78const getRouteConfigsFromChildren = (children, groupKey, groupOptions) => {
79 const configs = React.Children.toArray(children).reduce((acc, child) => {
80 var _child$type, _child$props;
81
82 if ( /*#__PURE__*/React.isValidElement(child)) {
83 if (child.type === _Screen.default) {
84 // We can only extract the config from `Screen` elements
85 // If something else was rendered, it's probably a bug
86 if (!isValidKey(child.props.navigationKey)) {
87 throw new Error(`Got an invalid 'navigationKey' prop (${JSON.stringify(child.props.navigationKey)}) for the screen '${child.props.name}'. It must be a non-empty string or 'undefined'.`);
88 }
89
90 acc.push({
91 keys: [groupKey, child.props.navigationKey],
92 options: groupOptions,
93 props: child.props
94 });
95 return acc;
96 }
97
98 if (child.type === React.Fragment || child.type === _Group.default) {
99 if (!isValidKey(child.props.navigationKey)) {
100 throw new Error(`Got an invalid 'navigationKey' prop (${JSON.stringify(child.props.navigationKey)}) for the group. It must be a non-empty string or 'undefined'.`);
101 } // When we encounter a fragment or group, we need to dive into its children to extract the configs
102 // This is handy to conditionally define a group of screens
103
104
105 acc.push(...getRouteConfigsFromChildren(child.props.children, child.props.navigationKey, child.type !== _Group.default ? groupOptions : groupOptions != null ? [...groupOptions, child.props.screenOptions] : [child.props.screenOptions]));
106 return acc;
107 }
108 }
109
110 throw new Error(`A navigator can only contain 'Screen', 'Group' or 'React.Fragment' as its direct children (found ${/*#__PURE__*/React.isValidElement(child) ? `'${typeof child.type === 'string' ? child.type : (_child$type = child.type) === null || _child$type === void 0 ? void 0 : _child$type.name}'${(_child$props = child.props) !== null && _child$props !== void 0 && _child$props.name ? ` for the screen '${child.props.name}'` : ''}` : typeof child === 'object' ? JSON.stringify(child) : `'${String(child)}'`}). To render this component in the navigator, pass it in the 'component' prop to 'Screen'.`);
111 }, []);
112
113 if (process.env.NODE_ENV !== 'production') {
114 configs.forEach(config => {
115 const {
116 name,
117 children,
118 component,
119 getComponent
120 } = config.props;
121
122 if (typeof name !== 'string' || !name) {
123 throw new Error(`Got an invalid name (${JSON.stringify(name)}) for the screen. It must be a non-empty string.`);
124 }
125
126 if (children != null || component !== undefined || getComponent !== undefined) {
127 if (children != null && component !== undefined) {
128 throw new Error(`Got both 'component' and 'children' props for the screen '${name}'. You must pass only one of them.`);
129 }
130
131 if (children != null && getComponent !== undefined) {
132 throw new Error(`Got both 'getComponent' and 'children' props for the screen '${name}'. You must pass only one of them.`);
133 }
134
135 if (component !== undefined && getComponent !== undefined) {
136 throw new Error(`Got both 'component' and 'getComponent' props for the screen '${name}'. You must pass only one of them.`);
137 }
138
139 if (children != null && typeof children !== 'function') {
140 throw new Error(`Got an invalid value for 'children' prop for the screen '${name}'. It must be a function returning a React Element.`);
141 }
142
143 if (component !== undefined && !(0, _reactIs.isValidElementType)(component)) {
144 throw new Error(`Got an invalid value for 'component' prop for the screen '${name}'. It must be a valid React Component.`);
145 }
146
147 if (getComponent !== undefined && typeof getComponent !== 'function') {
148 throw new Error(`Got an invalid value for 'getComponent' prop for the screen '${name}'. It must be a function returning a React Component.`);
149 }
150
151 if (typeof component === 'function') {
152 if (component.name === 'component') {
153 // Inline anonymous functions passed in the `component` prop will have the name of the prop
154 // It's relatively safe to assume that it's not a component since it should also have PascalCase name
155 // We won't catch all scenarios here, but this should catch a good chunk of incorrect use.
156 console.warn(`Looks like you're passing an inline function for 'component' prop for the screen '${name}' (e.g. component={() => <SomeComponent />}). Passing an inline function will cause the component state to be lost on re-render and cause perf issues since it's re-created every render. You can pass the function as children to 'Screen' instead to achieve the desired behaviour.`);
157 } else if (/^[a-z]/.test(component.name)) {
158 console.warn(`Got a component with the name '${component.name}' for the screen '${name}'. React Components must start with an uppercase letter. If you're passing a regular function and not a component, pass it as children to 'Screen' instead. Otherwise capitalize your component's name.`);
159 }
160 }
161 } else {
162 throw new Error(`Couldn't find a 'component', 'getComponent' or 'children' prop for the screen '${name}'. This can happen if you passed 'undefined'. You likely forgot to export your component from the file it's defined in, or mixed up default import and named import when importing.`);
163 }
164 });
165 }
166
167 return configs;
168};
169/**
170 * Hook for building navigators.
171 *
172 * @param createRouter Factory method which returns router object.
173 * @param options Options object containing `children` and additional options for the router.
174 * @returns An object containing `state`, `navigation`, `descriptors` objects.
175 */
176
177
178function useNavigationBuilder(createRouter, options) {
179 const navigatorKey = (0, _useRegisterNavigator.default)();
180 const route = React.useContext(_NavigationRouteContext.default);
181 const {
182 children,
183 screenListeners,
184 ...rest
185 } = options;
186 const {
187 current: router
188 } = React.useRef(createRouter({ ...rest,
189 ...(route !== null && route !== void 0 && route.params && route.params.state == null && route.params.initial !== false && typeof route.params.screen === 'string' ? {
190 initialRouteName: route.params.screen
191 } : null)
192 }));
193 const routeConfigs = getRouteConfigsFromChildren(children);
194 const screens = routeConfigs.reduce((acc, config) => {
195 if (config.props.name in acc) {
196 throw new Error(`A navigator cannot contain multiple 'Screen' components with the same name (found duplicate screen named '${config.props.name}')`);
197 }
198
199 acc[config.props.name] = config;
200 return acc;
201 }, {});
202 const routeNames = routeConfigs.map(config => config.props.name);
203 const routeKeyList = routeNames.reduce((acc, curr) => {
204 acc[curr] = screens[curr].keys.map(key => key !== null && key !== void 0 ? key : '').join(':');
205 return acc;
206 }, {});
207 const routeParamList = routeNames.reduce((acc, curr) => {
208 const {
209 initialParams
210 } = screens[curr].props;
211 acc[curr] = initialParams;
212 return acc;
213 }, {});
214 const routeGetIdList = routeNames.reduce((acc, curr) => Object.assign(acc, {
215 [curr]: screens[curr].props.getId
216 }), {});
217
218 if (!routeNames.length) {
219 throw new Error("Couldn't find any screens for the navigator. Have you defined any screens as its children?");
220 }
221
222 const isStateValid = React.useCallback(state => state.type === undefined || state.type === router.type, [router.type]);
223 const isStateInitialized = React.useCallback(state => state !== undefined && state.stale === false && isStateValid(state), [isStateValid]);
224 const {
225 state: currentState,
226 getState: getCurrentState,
227 setState: setCurrentState,
228 setKey,
229 getKey,
230 getIsInitial
231 } = React.useContext(_NavigationStateContext.default);
232 const stateCleanedUp = React.useRef(false);
233 const cleanUpState = React.useCallback(() => {
234 setCurrentState(undefined);
235 stateCleanedUp.current = true;
236 }, [setCurrentState]);
237 const setState = React.useCallback(state => {
238 if (stateCleanedUp.current) {
239 // State might have been already cleaned up due to unmount
240 // We do not want to expose API allowing to override this
241 // This would lead to old data preservation on main navigator unmount
242 return;
243 }
244
245 setCurrentState(state);
246 }, [setCurrentState]);
247 const [initializedState, isFirstStateInitialization] = React.useMemo(() => {
248 var _route$params4;
249
250 const initialRouteParamList = routeNames.reduce((acc, curr) => {
251 var _route$params, _route$params2, _route$params3;
252
253 const {
254 initialParams
255 } = screens[curr].props;
256 const initialParamsFromParams = (route === null || route === void 0 ? void 0 : (_route$params = route.params) === null || _route$params === void 0 ? void 0 : _route$params.state) == null && (route === null || route === void 0 ? void 0 : (_route$params2 = route.params) === null || _route$params2 === void 0 ? void 0 : _route$params2.initial) !== false && (route === null || route === void 0 ? void 0 : (_route$params3 = route.params) === null || _route$params3 === void 0 ? void 0 : _route$params3.screen) === curr ? route.params.params : undefined;
257 acc[curr] = initialParams !== undefined || initialParamsFromParams !== undefined ? { ...initialParams,
258 ...initialParamsFromParams
259 } : undefined;
260 return acc;
261 }, {}); // If the current state isn't initialized on first render, we initialize it
262 // We also need to re-initialize it if the state passed from parent was changed (maybe due to reset)
263 // Otherwise assume that the state was provided as initial state
264 // So we need to rehydrate it to make it usable
265
266 if ((currentState === undefined || !isStateValid(currentState)) && (route === null || route === void 0 ? void 0 : (_route$params4 = route.params) === null || _route$params4 === void 0 ? void 0 : _route$params4.state) == null) {
267 return [router.getInitialState({
268 routeNames,
269 routeParamList: initialRouteParamList,
270 routeGetIdList
271 }), true];
272 } else {
273 var _route$params$state, _route$params5;
274
275 return [router.getRehydratedState((_route$params$state = route === null || route === void 0 ? void 0 : (_route$params5 = route.params) === null || _route$params5 === void 0 ? void 0 : _route$params5.state) !== null && _route$params$state !== void 0 ? _route$params$state : currentState, {
276 routeNames,
277 routeParamList: initialRouteParamList,
278 routeGetIdList
279 }), false];
280 } // We explicitly don't include routeNames, route.params etc. in the dep list
281 // below. We want to avoid forcing a new state to be calculated in those cases
282 // Instead, we handle changes to these in the nextState code below. Note
283 // that some changes to routeConfigs are explicitly ignored, such as changes
284 // to initialParams
285 // eslint-disable-next-line react-hooks/exhaustive-deps
286
287 }, [currentState, router, isStateValid]);
288 const previousRouteKeyListRef = React.useRef(routeKeyList);
289 React.useEffect(() => {
290 previousRouteKeyListRef.current = routeKeyList;
291 });
292 const previousRouteKeyList = previousRouteKeyListRef.current;
293 let state = // If the state isn't initialized, or stale, use the state we initialized instead
294 // The state won't update until there's a change needed in the state we have initalized locally
295 // So it'll be `undefined` or stale until the first navigation event happens
296 isStateInitialized(currentState) ? currentState : initializedState;
297 let nextState = state;
298
299 if (!(0, _isArrayEqual.default)(state.routeNames, routeNames) || !(0, _isRecordEqual.default)(routeKeyList, previousRouteKeyList)) {
300 // When the list of route names change, the router should handle it to remove invalid routes
301 nextState = router.getStateForRouteNamesChange(state, {
302 routeNames,
303 routeParamList,
304 routeGetIdList,
305 routeKeyChanges: Object.keys(routeKeyList).filter(name => previousRouteKeyList.hasOwnProperty(name) && routeKeyList[name] !== previousRouteKeyList[name])
306 });
307 }
308
309 const previousNestedParamsRef = React.useRef(route === null || route === void 0 ? void 0 : route.params);
310 React.useEffect(() => {
311 previousNestedParamsRef.current = route === null || route === void 0 ? void 0 : route.params;
312 }, [route === null || route === void 0 ? void 0 : route.params]);
313
314 if (route !== null && route !== void 0 && route.params) {
315 const previousParams = previousNestedParamsRef.current;
316 let action;
317
318 if (typeof route.params.state === 'object' && route.params.state != null && route.params !== previousParams) {
319 // If the route was updated with new state, we should reset to it
320 action = _routers.CommonActions.reset(route.params.state);
321 } else if (typeof route.params.screen === 'string' && (route.params.initial === false && isFirstStateInitialization || route.params !== previousParams)) {
322 // If the route was updated with new screen name and/or params, we should navigate there
323 action = _routers.CommonActions.navigate({
324 name: route.params.screen,
325 params: route.params.params,
326 path: route.params.path
327 });
328 } // The update should be limited to current navigator only, so we call the router manually
329
330
331 const updatedState = action ? router.getStateForAction(nextState, action, {
332 routeNames,
333 routeParamList,
334 routeGetIdList
335 }) : null;
336 nextState = updatedState !== null ? router.getRehydratedState(updatedState, {
337 routeNames,
338 routeParamList,
339 routeGetIdList
340 }) : nextState;
341 }
342
343 const shouldUpdate = state !== nextState;
344 (0, _useScheduleUpdate.default)(() => {
345 if (shouldUpdate) {
346 // If the state needs to be updated, we'll schedule an update
347 setState(nextState);
348 }
349 }); // The up-to-date state will come in next render, but we don't need to wait for it
350 // We can't use the outdated state since the screens have changed, which will cause error due to mismatched config
351 // So we override the state object we return to use the latest state as soon as possible
352
353 state = nextState;
354 React.useEffect(() => {
355 setKey(navigatorKey);
356
357 if (!getIsInitial()) {
358 // If it's not initial render, we need to update the state
359 // This will make sure that our container gets notifier of state changes due to new mounts
360 // This is necessary for proper screen tracking, URL updates etc.
361 setState(nextState);
362 }
363
364 return () => {
365 // We need to clean up state for this navigator on unmount
366 // We do it in a timeout because we need to detect if another navigator mounted in the meantime
367 // For example, if another navigator has started rendering, we should skip cleanup
368 // Otherwise, our cleanup step will cleanup state for the other navigator and re-initialize it
369 setTimeout(() => {
370 if (getCurrentState() !== undefined && getKey() === navigatorKey) {
371 cleanUpState();
372 }
373 }, 0);
374 }; // eslint-disable-next-line react-hooks/exhaustive-deps
375 }, []); // We initialize this ref here to avoid a new getState getting initialized
376 // whenever initializedState changes. We want getState to have access to the
377 // latest initializedState, but don't need it to change when that happens
378
379 const initializedStateRef = React.useRef();
380 initializedStateRef.current = initializedState;
381 const getState = React.useCallback(() => {
382 const currentState = getCurrentState();
383 return isStateInitialized(currentState) ? currentState : initializedStateRef.current;
384 }, [getCurrentState, isStateInitialized]);
385 const emitter = (0, _useEventEmitter.default)(e => {
386 let routeNames = [];
387 let route;
388
389 if (e.target) {
390 var _route;
391
392 route = state.routes.find(route => route.key === e.target);
393
394 if ((_route = route) !== null && _route !== void 0 && _route.name) {
395 routeNames.push(route.name);
396 }
397 } else {
398 route = state.routes[state.index];
399 routeNames.push(...Object.keys(screens).filter(name => {
400 var _route2;
401
402 return ((_route2 = route) === null || _route2 === void 0 ? void 0 : _route2.name) === name;
403 }));
404 }
405
406 if (route == null) {
407 return;
408 }
409
410 const navigation = descriptors[route.key].navigation;
411 const listeners = [].concat( // Get an array of listeners for all screens + common listeners on navigator
412 ...[screenListeners, ...routeNames.map(name => {
413 const {
414 listeners
415 } = screens[name].props;
416 return listeners;
417 })].map(listeners => {
418 const map = typeof listeners === 'function' ? listeners({
419 route: route,
420 navigation
421 }) : listeners;
422 return map ? Object.keys(map).filter(type => type === e.type).map(type => map === null || map === void 0 ? void 0 : map[type]) : undefined;
423 })) // We don't want same listener to be called multiple times for same event
424 // So we remove any duplicate functions from the array
425 .filter((cb, i, self) => cb && self.lastIndexOf(cb) === i);
426 listeners.forEach(listener => listener === null || listener === void 0 ? void 0 : listener(e));
427 });
428 (0, _useFocusEvents.default)({
429 state,
430 emitter
431 });
432 React.useEffect(() => {
433 emitter.emit({
434 type: 'state',
435 data: {
436 state
437 }
438 });
439 }, [emitter, state]);
440 const {
441 listeners: childListeners,
442 addListener
443 } = (0, _useChildListeners.default)();
444 const {
445 keyedListeners,
446 addKeyedListener
447 } = (0, _useKeyedChildListeners.default)();
448 const onAction = (0, _useOnAction.default)({
449 router,
450 getState,
451 setState,
452 key: route === null || route === void 0 ? void 0 : route.key,
453 actionListeners: childListeners.action,
454 beforeRemoveListeners: keyedListeners.beforeRemove,
455 routerConfigOptions: {
456 routeNames,
457 routeParamList,
458 routeGetIdList
459 },
460 emitter
461 });
462 const onRouteFocus = (0, _useOnRouteFocus.default)({
463 router,
464 key: route === null || route === void 0 ? void 0 : route.key,
465 getState,
466 setState
467 });
468 const navigation = (0, _useNavigationHelpers.default)({
469 id: options.id,
470 onAction,
471 getState,
472 emitter,
473 router
474 });
475 (0, _useFocusedListenersChildrenAdapter.default)({
476 navigation,
477 focusedListeners: childListeners.focus
478 });
479 (0, _useOnGetState.default)({
480 getState,
481 getStateListeners: keyedListeners.getState
482 });
483 const descriptors = (0, _useDescriptors.default)({
484 state,
485 screens,
486 navigation,
487 screenOptions: options.screenOptions,
488 defaultScreenOptions: options.defaultScreenOptions,
489 onAction,
490 getState,
491 setState,
492 onRouteFocus,
493 addListener,
494 addKeyedListener,
495 router,
496 // @ts-expect-error: this should have both core and custom events, but too much work right now
497 emitter
498 });
499 (0, _useCurrentRender.default)({
500 state,
501 navigation,
502 descriptors
503 });
504 const NavigationContent = (0, _useComponent.default)(children => /*#__PURE__*/React.createElement(_NavigationHelpersContext.default.Provider, {
505 value: navigation
506 }, /*#__PURE__*/React.createElement(_PreventRemoveProvider.default, null, children)));
507 return {
508 state,
509 navigation,
510 descriptors,
511 NavigationContent
512 };
513}
514//# sourceMappingURL=useNavigationBuilder.js.map
\No newline at end of file