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

Commit 070378a

Browse files
authored
Merge pull request #38 from react-navigation/bug/fix-events
Bug/fix events
2 parents d766f74 + 24b1cb2 commit 070378a

File tree

2 files changed

+95
-46
lines changed

2 files changed

+95
-46
lines changed

src/Hooks.ts

Lines changed: 94 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,104 @@ 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
63+
// (that we use in <NavigationEvents/>)
64+
// When mounting/focusing a new screen and subscribing to focus, the focus event should be fired
65+
// It wouldn't fire if we did subscribe with useEffect()
66+
useLayoutEffect(() => {
67+
const subscribedCallback: NavigationEventCallback = event => {
68+
const latestCallback = getLatestCallback();
69+
latestCallback(event);
70+
};
71+
72+
const subs = [
73+
// TODO should we remove "action" here? it's not in the published typedefs
74+
navigation.addListener('action' as any, subscribedCallback),
75+
navigation.addListener('willFocus', subscribedCallback),
76+
navigation.addListener('didFocus', subscribedCallback),
77+
navigation.addListener('willBlur', subscribedCallback),
78+
navigation.addListener('didBlur', subscribedCallback),
79+
];
80+
return () => {
81+
subs.forEach(sub => sub.remove());
82+
};
83+
}, [navigation.state.key]);
84+
}
85+
86+
export interface FocusState {
87+
isFocused: boolean;
88+
isBlurring: boolean;
89+
isBlurred: boolean;
90+
isFocusing: boolean;
5791
}
5892

59-
const emptyFocusState = {
93+
const emptyFocusState: FocusState = {
6094
isFocused: false,
6195
isBlurring: false,
6296
isBlurred: false,
6397
isFocusing: false,
6498
};
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) {
99+
const didFocusState: FocusState = { ...emptyFocusState, isFocused: true };
100+
const willBlurState: FocusState = { ...emptyFocusState, isBlurring: true };
101+
const didBlurState: FocusState = { ...emptyFocusState, isBlurred: true };
102+
const willFocusState: FocusState = { ...emptyFocusState, isFocusing: true };
103+
104+
function nextFocusState(
105+
eventName: EventType,
106+
currentState: FocusState
107+
): FocusState {
72108
switch (eventName) {
109+
case 'willFocus':
110+
return {
111+
...willFocusState,
112+
// /!\ willFocus will fire on screen mount, while the screen is already marked as focused.
113+
// In case of a new screen mounted/focused, we want to avoid a isFocused = true => false => true transition
114+
// So we don't put the "false" here and ensure the attribute remains as before
115+
// Currently I think the behavior of the event system on mount is not very well specified
116+
// See also https://twitter.com/sebastienlorber/status/1166986080966578176
117+
isFocused: currentState.isFocused,
118+
};
73119
case 'didFocus':
74120
return didFocusState;
75-
case 'willFocus':
76-
return willFocusState;
77121
case 'willBlur':
78122
return willBlurState;
79123
case 'didBlur':
80124
return didBlurState;
81125
default:
82-
return null;
126+
// preserve current state for other events ("action"?)
127+
return currentState;
83128
}
84129
}
85130

86131
export function useFocusState() {
87132
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);
133+
134+
const [focusState, setFocusState] = useState<FocusState>(() => {
135+
return navigation.isFocused() ? didFocusState : didBlurState;
136+
});
137+
138+
useNavigationEvents((e: NavigationEventPayload) => {
139+
setFocusState(currentFocusState =>
140+
nextFocusState(e.type, currentFocusState)
141+
);
142+
});
143+
95144
return focusState;
96145
}

tslint.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"defaultSeverity": "error",
99
"jsRules": {},
1010
"rules": {
11-
"quotemark": [true, "single", "jsx-double"],
11+
"quotemark": [false],
1212
"ordered-imports": false,
1313
"object-literal-sort-keys": false,
1414
"arrow-parens": [true, "ban-single-arg-parens"],

0 commit comments

Comments
 (0)