diff --git a/example/src/Screens/AuthFlow.tsx b/example/src/Screens/AuthFlow.tsx index 2117a27d..7068d88e 100644 --- a/example/src/Screens/AuthFlow.tsx +++ b/example/src/Screens/AuthFlow.tsx @@ -147,6 +147,10 @@ export default function SimpleStackScreen({ [] ); + if (state.isLoading) { + return ; + } + return ( - {state.isLoading ? ( - - ) : state.userToken === undefined ? ( + {state.userToken === undefined ? ( isInitialRef.current, []); + const context = React.useMemo( () => ({ state, @@ -254,14 +258,24 @@ const BaseNavigationContainer = React.forwardRef( setState, getKey, setKey, + getIsInitial, addOptionsGetter, }), - [getKey, getState, setKey, setState, state, addOptionsGetter] + [ + state, + getState, + setState, + getKey, + setKey, + getIsInitial, + addOptionsGetter, + ] ); const onStateChangeRef = React.useRef(onStateChange); React.useEffect(() => { + isInitialRef.current = false; onStateChangeRef.current = onStateChange; }); diff --git a/packages/core/src/NavigationStateContext.tsx b/packages/core/src/NavigationStateContext.tsx index d0efbc48..d4191f45 100644 --- a/packages/core/src/NavigationStateContext.tsx +++ b/packages/core/src/NavigationStateContext.tsx @@ -13,6 +13,7 @@ export default React.createContext<{ setState: ( state: NavigationState | PartialState | undefined ) => void; + getIsInitial: () => boolean; addOptionsGetter?: ( key: string, getter: () => object | undefined | null @@ -32,4 +33,7 @@ export default React.createContext<{ get setState(): any { throw new Error(MISSING_CONTEXT_ERROR); }, + get getIsInitial(): any { + throw new Error(MISSING_CONTEXT_ERROR); + }, }); diff --git a/packages/core/src/SceneView.tsx b/packages/core/src/SceneView.tsx index 87e1c53f..df8cabdf 100644 --- a/packages/core/src/SceneView.tsx +++ b/packages/core/src/SceneView.tsx @@ -76,6 +76,14 @@ export default function SceneView< [getState, route.key, setState] ); + const isInitialRef = React.useRef(true); + + React.useEffect(() => { + isInitialRef.current = false; + }); + + const getIsInitial = React.useCallback(() => isInitialRef.current, []); + const context = React.useMemo( () => ({ state: route.state, @@ -83,14 +91,16 @@ export default function SceneView< setState: setCurrentState, getKey, setKey, + getIsInitial, addOptionsGetter, }), [ - getCurrentState, - getKey, route.state, + getCurrentState, setCurrentState, + getKey, setKey, + getIsInitial, addOptionsGetter, ] ); diff --git a/packages/core/src/__tests__/BaseNavigationContainer.test.tsx b/packages/core/src/__tests__/BaseNavigationContainer.test.tsx index 5df2ff20..8f5a108b 100644 --- a/packages/core/src/__tests__/BaseNavigationContainer.test.tsx +++ b/packages/core/src/__tests__/BaseNavigationContainer.test.tsx @@ -366,6 +366,7 @@ it('handles getRootState', () => { type: 'test', }); }); + it('emits state events when the state changes', () => { const TestNavigator = (props: any) => { const { state, descriptors } = useNavigationBuilder(MockRouter, props); @@ -401,6 +402,7 @@ it('emits state events when the state changes', () => { ref.current?.navigate('bar'); }); + expect(listener).toBeCalledTimes(1); expect(listener.mock.calls[0][0].data.state).toEqual({ type: 'test', stale: false, @@ -418,6 +420,7 @@ it('emits state events when the state changes', () => { ref.current?.navigate('baz', { answer: 42 }); }); + expect(listener).toBeCalledTimes(2); expect(listener.mock.calls[1][0].data.state).toEqual({ type: 'test', stale: false, @@ -432,6 +435,97 @@ it('emits state events when the state changes', () => { }); }); +it('emits state events when new navigator mounts', () => { + jest.useFakeTimers(); + + const TestNavigator = (props: any) => { + const { state, descriptors } = useNavigationBuilder(MockRouter, props); + + return ( + + {state.routes.map((route) => descriptors[route.key].render())} + + ); + }; + + const ref = React.createRef(); + + const NestedNavigator = () => { + const [isRendered, setIsRendered] = React.useState(false); + + React.useEffect(() => { + setTimeout(() => setIsRendered(true), 100); + }, []); + + if (!isRendered) { + return null; + } + + return ( + + {() => null} + {() => null} + + ); + }; + + const onStateChange = jest.fn(); + + const element = ( + + + {() => null} + + + + ); + + render(element).update(element); + + const listener = jest.fn(); + + ref.current?.addListener('state', listener); + + expect(listener).not.toHaveBeenCalled(); + expect(onStateChange).not.toHaveBeenCalled(); + + act(() => { + jest.runAllTimers(); + }); + + const resultState = { + stale: false, + type: 'test', + index: 0, + key: '10', + routeNames: ['foo', 'bar'], + routes: [ + { key: 'foo', name: 'foo' }, + { + key: 'bar', + name: 'bar', + state: { + stale: false, + type: 'test', + index: 0, + key: '11', + routeNames: ['baz', 'bax'], + routes: [ + { key: 'baz', name: 'baz' }, + { key: 'bax', name: 'bax' }, + ], + }, + }, + ], + }; + + expect(listener).toBeCalledTimes(1); + expect(listener.mock.calls[0][0].data.state).toEqual(resultState); + + expect(onStateChange).toBeCalledTimes(1); + expect(onStateChange).lastCalledWith(resultState); +}); + it('emits option events when options change with tab router', () => { const TestNavigator = (props: any) => { const { state, descriptors } = useNavigationBuilder(TabRouter, props); diff --git a/packages/core/src/useNavigationBuilder.tsx b/packages/core/src/useNavigationBuilder.tsx index 37d774a7..56520f4b 100644 --- a/packages/core/src/useNavigationBuilder.tsx +++ b/packages/core/src/useNavigationBuilder.tsx @@ -279,6 +279,7 @@ export default function useNavigationBuilder< setState, setKey, getKey, + getIsInitial, } = React.useContext(NavigationStateContext); const [initializedState, isFirstStateInitialization] = React.useMemo(() => { @@ -372,6 +373,13 @@ export default function useNavigationBuilder< React.useEffect(() => { setKey(navigatorKey); + if (!getIsInitial()) { + // If it's not initial render, we need to update the state + // This will make sure that our container gets notifier of state changes due to new mounts + // This is necessary for proper screen tracking, URL updates etc. + setState(nextState); + } + return () => { // We need to clean up state for this navigator on unmount // We do it in a timeout because we need to detect if another navigator mounted in the meantime