1
- import { useState , useContext , useEffect } from 'react' ;
1
+ import {
2
+ useState ,
3
+ useContext ,
4
+ useLayoutEffect ,
5
+ useRef ,
6
+ useCallback ,
7
+ } from 'react' ;
2
8
3
9
import {
4
10
NavigationContext ,
@@ -11,7 +17,15 @@ import {
11
17
} from 'react-navigation' ;
12
18
13
19
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 ;
15
29
}
16
30
17
31
export function useNavigationParam < T extends keyof NavigationParams > (
@@ -28,69 +42,104 @@ export function useNavigationKey() {
28
42
return useNavigation ( ) . state . key ;
29
43
}
30
44
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 ) {
32
55
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 ;
57
91
}
58
92
59
- const emptyFocusState = {
93
+ const emptyFocusState : FocusState = {
60
94
isFocused : false ,
61
95
isBlurring : false ,
62
96
isBlurred : false ,
63
97
isFocusing : false ,
64
98
} ;
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 {
72
108
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
+ } ;
73
119
case 'didFocus' :
74
120
return didFocusState ;
75
- case 'willFocus' :
76
- return willFocusState ;
77
121
case 'willBlur' :
78
122
return willBlurState ;
79
123
case 'didBlur' :
80
124
return didBlurState ;
81
125
default :
82
- return null ;
126
+ // preserve current state for other events ("action"?)
127
+ return currentState ;
83
128
}
84
129
}
85
130
86
131
export function useFocusState ( ) {
87
132
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
+
95
144
return focusState ;
96
145
}
0 commit comments