UNPKG

4.82 kBTypeScriptView Raw
1import type {
2 NavigationAction,
3 NavigationState,
4 PartialState,
5 Router,
6 RouterConfigOptions,
7} from '@react-navigation/routers';
8import * as React from 'react';
9
10import NavigationBuilderContext, {
11 ChildActionListener,
12 ChildBeforeRemoveListener,
13} from './NavigationBuilderContext';
14import type { EventMapCore } from './types';
15import type { NavigationEventEmitter } from './useEventEmitter';
16import useOnPreventRemove, { shouldPreventRemove } from './useOnPreventRemove';
17
18type Options = {
19 router: Router<NavigationState, NavigationAction>;
20 key?: string;
21 getState: () => NavigationState;
22 setState: (state: NavigationState | PartialState<NavigationState>) => void;
23 actionListeners: ChildActionListener[];
24 beforeRemoveListeners: Record<string, ChildBeforeRemoveListener | undefined>;
25 routerConfigOptions: RouterConfigOptions;
26 emitter: NavigationEventEmitter<EventMapCore<any>>;
27};
28
29/**
30 * Hook to handle actions for a navigator, including state updates and bubbling.
31 *
32 * Bubbling an action is achieved in 2 ways:
33 * 1. To bubble action to parent, we expose the action handler in context and then access the parent context
34 * 2. To bubble action to child, child adds event listeners subscribing to actions from parent
35 *
36 * When the action handler handles as action, it returns `true`, otherwise `false`.
37 */
38export default function useOnAction({
39 router,
40 getState,
41 setState,
42 key,
43 actionListeners,
44 beforeRemoveListeners,
45 routerConfigOptions,
46 emitter,
47}: Options) {
48 const {
49 onAction: onActionParent,
50 onRouteFocus: onRouteFocusParent,
51 addListener: addListenerParent,
52 onDispatchAction,
53 } = React.useContext(NavigationBuilderContext);
54
55 const routerConfigOptionsRef =
56 React.useRef<RouterConfigOptions>(routerConfigOptions);
57
58 React.useEffect(() => {
59 routerConfigOptionsRef.current = routerConfigOptions;
60 });
61
62 const onAction = React.useCallback(
63 (
64 action: NavigationAction,
65 visitedNavigators: Set<string> = new Set<string>()
66 ) => {
67 const state = getState();
68
69 // Since actions can bubble both up and down, they could come to the same navigator again
70 // We keep track of navigators which have already tried to handle the action and return if it's already visited
71 if (visitedNavigators.has(state.key)) {
72 return false;
73 }
74
75 visitedNavigators.add(state.key);
76
77 if (typeof action.target !== 'string' || action.target === state.key) {
78 let result = router.getStateForAction(
79 state,
80 action,
81 routerConfigOptionsRef.current
82 );
83
84 // If a target is specified and set to current navigator, the action shouldn't bubble
85 // So instead of `null`, we use the state object for such cases to signal that action was handled
86 result =
87 result === null && action.target === state.key ? state : result;
88
89 if (result !== null) {
90 onDispatchAction(action, state === result);
91
92 if (state !== result) {
93 const isPrevented = shouldPreventRemove(
94 emitter,
95 beforeRemoveListeners,
96 state.routes,
97 result.routes,
98 action
99 );
100
101 if (isPrevented) {
102 return true;
103 }
104
105 setState(result);
106 }
107
108 if (onRouteFocusParent !== undefined) {
109 // Some actions such as `NAVIGATE` also want to bring the navigated route to focus in the whole tree
110 // This means we need to focus all of the parent navigators of this navigator as well
111 const shouldFocus = router.shouldActionChangeFocus(action);
112
113 if (shouldFocus && key !== undefined) {
114 onRouteFocusParent(key);
115 }
116 }
117
118 return true;
119 }
120 }
121
122 if (onActionParent !== undefined) {
123 // Bubble action to the parent if the current navigator didn't handle it
124 if (onActionParent(action, visitedNavigators)) {
125 return true;
126 }
127 }
128
129 // If the action wasn't handled by current navigator or a parent navigator, let children handle it
130 for (let i = actionListeners.length - 1; i >= 0; i--) {
131 const listener = actionListeners[i];
132
133 if (listener(action, visitedNavigators)) {
134 return true;
135 }
136 }
137
138 return false;
139 },
140 [
141 actionListeners,
142 beforeRemoveListeners,
143 emitter,
144 getState,
145 key,
146 onActionParent,
147 onDispatchAction,
148 onRouteFocusParent,
149 router,
150 setState,
151 ]
152 );
153
154 useOnPreventRemove({
155 getState,
156 emitter,
157 beforeRemoveListeners,
158 });
159
160 React.useEffect(
161 () => addListenerParent?.('action', onAction),
162 [addListenerParent, onAction]
163 );
164
165 return onAction;
166}