1- import { useState , useContext , useEffect } from 'react' ;
1+ import {
2+ useState ,
3+ useContext ,
4+ useLayoutEffect ,
5+ useRef ,
6+ useCallback ,
7+ } from 'react' ;
28
39import {
410 NavigationContext ,
@@ -11,7 +17,15 @@ import {
1117} from 'react-navigation' ;
1218
1319export 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
1731export 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
86130export 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