Skip to content
This repository was archived by the owner on Dec 3, 2022. It is now read-only.

Commit 0adf086

Browse files
committed
fix the event system / focus hooks
1 parent d766f74 commit 0adf086

File tree

1 file changed

+93
-45
lines changed

1 file changed

+93
-45
lines changed

src/Hooks.ts

Lines changed: 93 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { useState, useContext, useEffect } from 'react';
1+
import {
2+
useState,
3+
useContext,
4+
useLayoutEffect,
5+
useRef,
6+
useCallback,
7+
} from 'react';
28

39
import {
410
NavigationContext,
@@ -11,7 +17,15 @@ import {
1117
} from 'react-navigation';
1218

1319
export function useNavigation<S>(): NavigationScreenProp<S & NavigationRoute> {
14-
return useContext(NavigationContext as any);
20+
const navigation = useContext(NavigationContext) as any; // TODO typing?
21+
if (!navigation) {
22+
throw new Error(
23+
"react-navigation hooks require a navigation context but it couldn't be found. " +
24+
"Make sure you didn't forget to create and render the react-navigation app container. " +
25+
'If you need to access an optional navigation object, you can useContext(NavigationContext), which may return'
26+
);
27+
}
28+
return navigation;
1529
}
1630

1731
export function useNavigationParam<T extends keyof NavigationParams>(
@@ -28,69 +42,103 @@ export function useNavigationKey() {
2842
return useNavigation().state.key;
2943
}
3044

31-
export function useNavigationEvents(handleEvt: NavigationEventCallback) {
45+
// Useful to access the latest user-provided value
46+
const useGetter = <S>(value: S): (() => S) => {
47+
const ref = useRef(value);
48+
useLayoutEffect(() => {
49+
ref.current = value;
50+
});
51+
return useCallback(() => ref.current, [ref]);
52+
};
53+
54+
export function useNavigationEvents(callback: NavigationEventCallback) {
3255
const navigation = useNavigation();
33-
useEffect(
34-
() => {
35-
const subsA = navigation.addListener(
36-
'action' as any // TODO should we remove it? it's not in the published typedefs
37-
, handleEvt);
38-
const subsWF = navigation.addListener('willFocus', handleEvt);
39-
const subsDF = navigation.addListener('didFocus', handleEvt);
40-
const subsWB = navigation.addListener('willBlur', handleEvt);
41-
const subsDB = navigation.addListener('didBlur', handleEvt);
42-
return () => {
43-
subsA.remove();
44-
subsWF.remove();
45-
subsDF.remove();
46-
subsWB.remove();
47-
subsDB.remove();
48-
};
49-
},
50-
// For TODO consideration: If the events are tied to the navigation object and the key
51-
// identifies the nav object, then we should probably pass [navigation.state.key] here, to
52-
// make sure react doesn't needlessly detach and re-attach this effect. In practice this
53-
// seems to cause troubles
54-
undefined
55-
// [navigation.state.key]
56-
);
56+
57+
// Closure might change over time and capture some variables
58+
// It's important to fire the latest closure provided by the user
59+
const getLatestCallback = useGetter(callback);
60+
61+
// It's important to useLayoutEffect because we want to ensure we subscribe synchronously to the mounting
62+
// of the component, similarly to what would happen if we did use componentDidMount (that we use in <NavigationEvents/>)
63+
// When mounting/focusing a new screen and subscribing to focus, the focus event should be fired
64+
// It wouldn't fire if we did subscribe with useEffect()
65+
useLayoutEffect(() => {
66+
const subscribedCallback: NavigationEventCallback = evt => {
67+
const latestEventHandler = getLatestCallback();
68+
latestEventHandler(evt);
69+
};
70+
71+
const subs = [
72+
// TODO should we remove "action" here? it's not in the published typedefs
73+
navigation.addListener('action' as any, subscribedCallback),
74+
navigation.addListener('willFocus', subscribedCallback),
75+
navigation.addListener('didFocus', subscribedCallback),
76+
navigation.addListener('willBlur', subscribedCallback),
77+
navigation.addListener('didBlur', subscribedCallback),
78+
];
79+
return () => {
80+
subs.forEach(sub => sub.remove());
81+
};
82+
}, [navigation.state.key]);
5783
}
5884

59-
const emptyFocusState = {
85+
export type FocusState = {
86+
isFocused: boolean;
87+
isBlurring: boolean;
88+
isBlurred: boolean;
89+
isFocusing: boolean;
90+
};
91+
92+
const emptyFocusState: FocusState = {
6093
isFocused: false,
6194
isBlurring: false,
6295
isBlurred: false,
6396
isFocusing: false,
6497
};
65-
const didFocusState = { ...emptyFocusState, isFocused: true };
66-
const willBlurState = { ...emptyFocusState, isBlurring: true };
67-
const didBlurState = { ...emptyFocusState, isBlurred: true };
68-
const willFocusState = { ...emptyFocusState, isFocusing: true };
69-
const getInitialFocusState = (isFocused: boolean) =>
70-
isFocused ? didFocusState : didBlurState;
71-
function focusStateOfEvent(eventName: EventType) {
98+
const didFocusState: FocusState = { ...emptyFocusState, isFocused: true };
99+
const willBlurState: FocusState = { ...emptyFocusState, isBlurring: true };
100+
const didBlurState: FocusState = { ...emptyFocusState, isBlurred: true };
101+
const willFocusState: FocusState = { ...emptyFocusState, isFocusing: true };
102+
103+
function nextFocusState(
104+
eventName: EventType,
105+
currentState: FocusState
106+
): FocusState {
72107
switch (eventName) {
108+
case 'willFocus':
109+
return {
110+
...willFocusState,
111+
// /!\ willFocus will fire on screen mount, while the screen is already marked as focused.
112+
// In case of a new screen mounted/focused, we want to avoid a isFocused = true => false => true transition
113+
// So we don't put the "false" here and ensure the attribute remains as before
114+
// Currently I think the behavior of the event system on mount is not very well specified
115+
// See also https://twitter.com/sebastienlorber/status/1166986080966578176
116+
isFocused: currentState.isFocused,
117+
};
73118
case 'didFocus':
74119
return didFocusState;
75-
case 'willFocus':
76-
return willFocusState;
77120
case 'willBlur':
78121
return willBlurState;
79122
case 'didBlur':
80123
return didBlurState;
81124
default:
82-
return null;
125+
// preserve current state for other events ("action"?)
126+
return currentState;
83127
}
84128
}
85129

86130
export function useFocusState() {
87131
const navigation = useNavigation();
88-
const isFocused = navigation.isFocused();
89-
const [focusState, setFocusState] = useState(getInitialFocusState(isFocused));
90-
function handleEvt(e: NavigationEventPayload) {
91-
const newState = focusStateOfEvent(e.type);
92-
newState && setFocusState(newState);
93-
}
94-
useNavigationEvents(handleEvt);
132+
133+
const [focusState, setFocusState] = useState<FocusState>(() => {
134+
return navigation.isFocused() ? didFocusState : didBlurState;
135+
});
136+
137+
useNavigationEvents((e: NavigationEventPayload) => {
138+
setFocusState(currentFocusState =>
139+
nextFocusState(e.type, currentFocusState)
140+
);
141+
});
142+
95143
return focusState;
96144
}

0 commit comments

Comments
 (0)