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,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
86131export 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}
0 commit comments