This repository was archived by the owner on Dec 3, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 35
/
Copy pathHooks.ts
218 lines (186 loc) · 6.46 KB
/
Hooks.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
import {
useState,
useContext,
useEffect,
useLayoutEffect,
useRef,
useCallback,
} from 'react';
import {
NavigationContext,
NavigationScreenProp,
NavigationRoute,
NavigationParams,
NavigationEventCallback,
NavigationEventPayload,
EventType,
} from 'react-navigation';
type NavigationType<S> = S & NavigationScreenProp<NavigationRoute>;
type NavigationWithParamType<S, P> = NavigationType<S> & NavigationScreenProp<NavigationRoute, P>;
export function useNavigation<S>(): NavigationType<S>;
export function useNavigation<S, P>(): NavigationWithParamType<S, P>;
export function useNavigation() {
const navigation = useContext(NavigationContext);
if (!navigation) {
throw new Error(
"react-navigation hooks require a navigation context but it couldn't be found. " +
"Make sure you didn't forget to create and render the react-navigation app container. " +
'If you need to access an optional navigation object, you can useContext(NavigationContext), which may return'
);
}
return navigation;
}
export function useNavigationParam<T extends keyof NavigationParams>(
paramName: T
) {
return useNavigation().getParam(paramName);
}
export function useNavigationState() {
return useNavigation().state;
}
export function useNavigationKey() {
return useNavigation().state.key;
}
// Useful to access the latest user-provided value
const useGetter = <S>(value: S): (() => S) => {
const ref = useRef(value);
useLayoutEffect(() => {
ref.current = value;
});
return useCallback(() => ref.current, [ref]);
};
export function useNavigationEvents(callback: NavigationEventCallback) {
const navigation = useNavigation();
// Closure might change over time and capture some variables
// It's important to fire the latest closure provided by the user
const getLatestCallback = useGetter(callback);
// It's important to useLayoutEffect because we want to ensure we subscribe synchronously to the mounting
// of the component, similarly to what would happen if we did use componentDidMount
// (that we use in <NavigationEvents/>)
// When mounting/focusing a new screen and subscribing to focus, the focus event should be fired
// It wouldn't fire if we did subscribe with useEffect()
useLayoutEffect(() => {
const subscribedCallback: NavigationEventCallback = event => {
const latestCallback = getLatestCallback();
latestCallback(event);
};
const subs = [
// TODO should we remove "action" here? it's not in the published typedefs
navigation.addListener('action' as any, subscribedCallback),
navigation.addListener('willFocus', subscribedCallback),
navigation.addListener('didFocus', subscribedCallback),
navigation.addListener('willBlur', subscribedCallback),
navigation.addListener('didBlur', subscribedCallback),
];
return () => {
subs.forEach(sub => sub.remove());
};
}, [navigation.state.key]);
}
export interface FocusState {
isFocused: boolean;
isBlurring: boolean;
isBlurred: boolean;
isFocusing: boolean;
}
const emptyFocusState: FocusState = {
isFocused: false,
isBlurring: false,
isBlurred: false,
isFocusing: false,
};
const didFocusState: FocusState = { ...emptyFocusState, isFocused: true };
const willBlurState: FocusState = { ...emptyFocusState, isBlurring: true };
const didBlurState: FocusState = { ...emptyFocusState, isBlurred: true };
const willFocusState: FocusState = { ...emptyFocusState, isFocusing: true };
function nextFocusState(
eventName: EventType,
currentState: FocusState
): FocusState {
switch (eventName) {
case 'willFocus':
return {
...willFocusState,
// /!\ willFocus will fire on screen mount, while the screen is already marked as focused.
// In case of a new screen mounted/focused, we want to avoid a isFocused = true => false => true transition
// So we don't put the "false" here and ensure the attribute remains as before
// Currently I think the behavior of the event system on mount is not very well specified
// See also https://twitter.com/sebastienlorber/status/1166986080966578176
isFocused: currentState.isFocused,
};
case 'didFocus':
return didFocusState;
case 'willBlur':
return willBlurState;
case 'didBlur':
return didBlurState;
default:
// preserve current state for other events ("action"?)
return currentState;
}
}
export function useFocusState() {
const navigation = useNavigation();
const [focusState, setFocusState] = useState<FocusState>(() => {
return navigation.isFocused() ? didFocusState : didBlurState;
});
useNavigationEvents((e: NavigationEventPayload) => {
setFocusState(currentFocusState =>
nextFocusState(e.type, currentFocusState)
);
});
return focusState;
}
type EffectCallback = (() => void) | (() => () => void);
// Inspired by same hook from react-navigation v5
// See https://github.com/react-navigation/hooks/issues/39#issuecomment-534694135
export const useFocusEffect = (callback: EffectCallback) => {
const navigation = useNavigation();
useEffect(() => {
let isFocused = false;
let cleanup: (() => void) | void;
if (navigation.isFocused()) {
cleanup = callback();
isFocused = true;
}
const focusSubscription = navigation.addListener('willFocus', () => {
// If callback was already called for focus, avoid calling it again
// The focus event may also fire on intial render, so we guard against runing the effect twice
if (isFocused) {
return;
}
cleanup && cleanup();
cleanup = callback();
isFocused = true;
});
const blurSubscription = navigation.addListener('willBlur', () => {
cleanup && cleanup();
cleanup = undefined;
isFocused = false;
});
return () => {
cleanup && cleanup();
focusSubscription.remove();
blurSubscription.remove();
};
}, [callback, navigation]);
};
export const useIsFocused = () => {
const navigation = useNavigation();
const getNavigation = useGetter(navigation);
const [focused, setFocused] = useState(navigation.isFocused);
useEffect(() => {
const nav = getNavigation();
const focusSubscription = nav.addListener('willFocus', () =>
setFocused(true)
);
const blurSubscription = nav.addListener('willBlur', () =>
setFocused(false)
);
return () => {
focusSubscription.remove();
blurSubscription.remove();
};
}, [getNavigation]);
return focused;
};