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,103 @@ 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 (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 ] ) ;
57
83
}
58
84
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 = {
60
93
isFocused : false ,
61
94
isBlurring : false ,
62
95
isBlurred : false ,
63
96
isFocusing : false ,
64
97
} ;
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 {
72
107
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
+ } ;
73
118
case 'didFocus' :
74
119
return didFocusState ;
75
- case 'willFocus' :
76
- return willFocusState ;
77
120
case 'willBlur' :
78
121
return willBlurState ;
79
122
case 'didBlur' :
80
123
return didBlurState ;
81
124
default :
82
- return null ;
125
+ // preserve current state for other events ("action"?)
126
+ return currentState ;
83
127
}
84
128
}
85
129
86
130
export function useFocusState ( ) {
87
131
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
+
95
143
return focusState ;
96
144
}
0 commit comments