diff --git a/example/src/index.tsx b/example/src/index.tsx index 4bcf7e29..2c69bb47 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -22,7 +22,7 @@ import { InitialState, useLinking, NavigationContainerRef, - NavigationNativeContainer, + NavigationContainer, DefaultTheme, DarkTheme, } from '@react-navigation/native'; @@ -202,7 +202,7 @@ export default function App() { {Platform.OS === 'ios' && ( )} - @@ -307,7 +307,7 @@ export default function App() { )} - + ); } diff --git a/packages/core/src/BaseNavigationContainer.tsx b/packages/core/src/BaseNavigationContainer.tsx new file mode 100644 index 00000000..32fed271 --- /dev/null +++ b/packages/core/src/BaseNavigationContainer.tsx @@ -0,0 +1,300 @@ +import * as React from 'react'; +import * as CommonActions from './CommonActions'; +import EnsureSingleNavigator from './EnsureSingleNavigator'; +import NavigationBuilderContext from './NavigationBuilderContext'; +import useFocusedListeners from './useFocusedListeners'; +import useDevTools from './useDevTools'; +import useStateGetters from './useStateGetters'; +import isSerializable from './isSerializable'; + +import { + Route, + NavigationState, + InitialState, + PartialState, + NavigationAction, + NavigationContainerRef, + NavigationContainerProps, +} from './types'; +import useEventEmitter from './useEventEmitter'; + +type State = NavigationState | PartialState | undefined; + +const MISSING_CONTEXT_ERROR = + "We couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?"; + +export const NavigationStateContext = React.createContext<{ + isDefault?: true; + state?: NavigationState | PartialState; + getState: () => NavigationState | PartialState | undefined; + setState: ( + state: NavigationState | PartialState | undefined + ) => void; + key?: string; + performTransaction: (action: () => void) => void; +}>({ + isDefault: true, + + get getState(): any { + throw new Error(MISSING_CONTEXT_ERROR); + }, + get setState(): any { + throw new Error(MISSING_CONTEXT_ERROR); + }, + get performTransaction(): any { + throw new Error(MISSING_CONTEXT_ERROR); + }, +}); + +let hasWarnedForSerialization = false; + +/** + * Remove `key` and `routeNames` from the state objects recursively to get partial state. + * + * @param state Initial state object. + */ +const getPartialState = ( + state: InitialState | undefined +): PartialState | undefined => { + if (state === undefined) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { key, routeNames, ...partialState } = state; + + // @ts-ignore + return { + ...partialState, + stale: true, + routes: state.routes.map(route => { + if (route.state === undefined) { + return route as Route & { + state?: PartialState; + }; + } + + return { ...route, state: getPartialState(route.state) }; + }), + }; +}; + +/** + * Container component which holds the navigation state. + * This should be rendered at the root wrapping the whole app. + * + * @param props.initialState Initial state object for the navigation tree. + * @param props.onStateChange Callback which is called with the latest navigation state when it changes. + * @param props.children Child elements to render the content. + * @param props.ref Ref object which refers to the navigation object containing helper methods. + */ +const BaseNavigationContainer = React.forwardRef( + function BaseNavigationContainer( + { + initialState, + onStateChange, + independent, + children, + }: NavigationContainerProps, + ref: React.Ref + ) { + const parent = React.useContext(NavigationStateContext); + + if (!parent.isDefault && !independent) { + throw new Error( + "Looks like you have nested a 'NavigationContainer' inside another. Normally you need only one container at the root of the app, so this was probably an error. If this was intentional, pass 'independent={true}' explicitely." + ); + } + + const [state, setNavigationState] = React.useState(() => + getPartialState(initialState == null ? undefined : initialState) + ); + + const navigationStateRef = React.useRef(); + const transactionStateRef = React.useRef(null); + const isTransactionActiveRef = React.useRef(false); + const isFirstMountRef = React.useRef(true); + const skipTrackingRef = React.useRef(false); + + const performTransaction = React.useCallback((callback: () => void) => { + if (isTransactionActiveRef.current) { + throw new Error( + "Only one transaction can be active at a time. Did you accidentally nest 'performTransaction'?" + ); + } + + setNavigationState((navigationState: State) => { + isTransactionActiveRef.current = true; + transactionStateRef.current = navigationState; + + try { + callback(); + } finally { + isTransactionActiveRef.current = false; + } + + return transactionStateRef.current; + }); + }, []); + + const getState = React.useCallback( + () => + transactionStateRef.current !== null + ? transactionStateRef.current + : navigationStateRef.current, + [] + ); + + const setState = React.useCallback((navigationState: State) => { + if (transactionStateRef.current === null) { + throw new Error( + "Any 'setState' calls need to be done inside 'performTransaction'" + ); + } + + transactionStateRef.current = navigationState; + }, []); + + const reset = React.useCallback( + (state: NavigationState) => { + performTransaction(() => { + skipTrackingRef.current = true; + setState(state); + }); + }, + [performTransaction, setState] + ); + + const { trackState, trackAction } = useDevTools({ + name: '@react-navigation', + reset, + state, + }); + + const { + listeners, + addListener: addFocusedListener, + } = useFocusedListeners(); + + const { getStateForRoute, addStateGetter } = useStateGetters(); + + const dispatch = ( + action: NavigationAction | ((state: NavigationState) => NavigationAction) + ) => { + listeners[0](navigation => navigation.dispatch(action)); + }; + + const canGoBack = () => { + const { result, handled } = listeners[0](navigation => + navigation.canGoBack() + ); + + if (handled) { + return result; + } else { + return false; + } + }; + + const resetRoot = React.useCallback( + (state?: PartialState | NavigationState) => { + performTransaction(() => { + trackAction('@@RESET_ROOT'); + setState(state); + }); + }, + [performTransaction, setState, trackAction] + ); + + const getRootState = React.useCallback(() => { + return getStateForRoute('root'); + }, [getStateForRoute]); + + const emitter = useEventEmitter(); + + React.useImperativeHandle(ref, () => ({ + ...(Object.keys(CommonActions) as (keyof typeof CommonActions)[]).reduce< + any + >((acc, name) => { + acc[name] = (...args: any[]) => + dispatch( + CommonActions[name]( + // @ts-ignore + ...args + ) + ); + return acc; + }, {}), + ...emitter.create('root'), + resetRoot, + dispatch, + canGoBack, + getRootState, + })); + + const builderContext = React.useMemo( + () => ({ + addFocusedListener, + addStateGetter, + trackAction, + }), + [addFocusedListener, trackAction, addStateGetter] + ); + + const context = React.useMemo( + () => ({ + state, + performTransaction, + getState, + setState, + }), + [getState, performTransaction, setState, state] + ); + + React.useEffect(() => { + if (process.env.NODE_ENV !== 'production') { + if ( + state !== undefined && + !isSerializable(state) && + !hasWarnedForSerialization + ) { + hasWarnedForSerialization = true; + + console.warn( + "We found non-serializable values in the navigation state, which can break usage such as persisting and restoring state. This might happen if you passed non-serializable values such as function, class instances etc. in params. If you need to use functions in your options, you can use 'navigation.setOptions' instead." + ); + } + } + + emitter.emit({ + type: 'state', + data: { state }, + }); + + if (skipTrackingRef.current) { + skipTrackingRef.current = false; + } else { + trackState(getRootState); + } + + navigationStateRef.current = state; + transactionStateRef.current = null; + + if (!isFirstMountRef.current && onStateChange) { + onStateChange(getRootState()); + } + + isFirstMountRef.current = false; + }, [state, onStateChange, trackState, getRootState, emitter]); + + return ( + + + {children} + + + ); + } +); + +export default BaseNavigationContainer; diff --git a/packages/core/src/NavigationContainer.tsx b/packages/core/src/NavigationContainer.tsx deleted file mode 100644 index a61d6a52..00000000 --- a/packages/core/src/NavigationContainer.tsx +++ /dev/null @@ -1,295 +0,0 @@ -import * as React from 'react'; -import * as CommonActions from './CommonActions'; -import EnsureSingleNavigator from './EnsureSingleNavigator'; -import NavigationBuilderContext from './NavigationBuilderContext'; -import useFocusedListeners from './useFocusedListeners'; -import useDevTools from './useDevTools'; -import useStateGetters from './useStateGetters'; -import isSerializable from './isSerializable'; - -import { - Route, - NavigationState, - InitialState, - PartialState, - NavigationAction, - NavigationContainerRef, - NavigationContainerProps, -} from './types'; -import useEventEmitter from './useEventEmitter'; - -type State = NavigationState | PartialState | undefined; - -const MISSING_CONTEXT_ERROR = - "We couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?"; - -export const NavigationStateContext = React.createContext<{ - isDefault?: true; - state?: NavigationState | PartialState; - getState: () => NavigationState | PartialState | undefined; - setState: ( - state: NavigationState | PartialState | undefined - ) => void; - key?: string; - performTransaction: (action: () => void) => void; -}>({ - isDefault: true, - - get getState(): any { - throw new Error(MISSING_CONTEXT_ERROR); - }, - get setState(): any { - throw new Error(MISSING_CONTEXT_ERROR); - }, - get performTransaction(): any { - throw new Error(MISSING_CONTEXT_ERROR); - }, -}); - -let hasWarnedForSerialization = false; - -/** - * Remove `key` and `routeNames` from the state objects recursively to get partial state. - * - * @param state Initial state object. - */ -const getPartialState = ( - state: InitialState | undefined -): PartialState | undefined => { - if (state === undefined) { - return; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { key, routeNames, ...partialState } = state; - - // @ts-ignore - return { - ...partialState, - stale: true, - routes: state.routes.map(route => { - if (route.state === undefined) { - return route as Route & { - state?: PartialState; - }; - } - - return { ...route, state: getPartialState(route.state) }; - }), - }; -}; - -/** - * Container component which holds the navigation state. - * This should be rendered at the root wrapping the whole app. - * - * @param props.initialState Initial state object for the navigation tree. - * @param props.onStateChange Callback which is called with the latest navigation state when it changes. - * @param props.children Child elements to render the content. - * @param props.ref Ref object which refers to the navigation object containing helper methods. - */ -const Container = React.forwardRef(function NavigationContainer( - { - initialState, - onStateChange, - independent, - children, - }: NavigationContainerProps, - ref: React.Ref -) { - const parent = React.useContext(NavigationStateContext); - - if (!parent.isDefault && !independent) { - throw new Error( - "Looks like you have nested a 'NavigationContainer' inside another. Normally you need only one container at the root of the app, so this was probably an error. If this was intentional, pass 'independent={true}' explicitely." - ); - } - - const [state, setNavigationState] = React.useState(() => - getPartialState(initialState == null ? undefined : initialState) - ); - - const navigationStateRef = React.useRef(); - const transactionStateRef = React.useRef(null); - const isTransactionActiveRef = React.useRef(false); - const isFirstMountRef = React.useRef(true); - const skipTrackingRef = React.useRef(false); - - const performTransaction = React.useCallback((callback: () => void) => { - if (isTransactionActiveRef.current) { - throw new Error( - "Only one transaction can be active at a time. Did you accidentally nest 'performTransaction'?" - ); - } - - setNavigationState((navigationState: State) => { - isTransactionActiveRef.current = true; - transactionStateRef.current = navigationState; - - try { - callback(); - } finally { - isTransactionActiveRef.current = false; - } - - return transactionStateRef.current; - }); - }, []); - - const getState = React.useCallback( - () => - transactionStateRef.current !== null - ? transactionStateRef.current - : navigationStateRef.current, - [] - ); - - const setState = React.useCallback((navigationState: State) => { - if (transactionStateRef.current === null) { - throw new Error( - "Any 'setState' calls need to be done inside 'performTransaction'" - ); - } - - transactionStateRef.current = navigationState; - }, []); - - const reset = React.useCallback( - (state: NavigationState) => { - performTransaction(() => { - skipTrackingRef.current = true; - setState(state); - }); - }, - [performTransaction, setState] - ); - - const { trackState, trackAction } = useDevTools({ - name: '@react-navigation', - reset, - state, - }); - - const { listeners, addListener: addFocusedListener } = useFocusedListeners(); - - const { getStateForRoute, addStateGetter } = useStateGetters(); - - const dispatch = ( - action: NavigationAction | ((state: NavigationState) => NavigationAction) - ) => { - listeners[0](navigation => navigation.dispatch(action)); - }; - - const canGoBack = () => { - const { result, handled } = listeners[0](navigation => - navigation.canGoBack() - ); - - if (handled) { - return result; - } else { - return false; - } - }; - - const resetRoot = React.useCallback( - (state?: PartialState | NavigationState) => { - performTransaction(() => { - trackAction('@@RESET_ROOT'); - setState(state); - }); - }, - [performTransaction, setState, trackAction] - ); - - const getRootState = React.useCallback(() => { - return getStateForRoute('root'); - }, [getStateForRoute]); - - const emitter = useEventEmitter(); - - React.useImperativeHandle(ref, () => ({ - ...(Object.keys(CommonActions) as (keyof typeof CommonActions)[]).reduce< - any - >((acc, name) => { - acc[name] = (...args: any[]) => - dispatch( - CommonActions[name]( - // @ts-ignore - ...args - ) - ); - return acc; - }, {}), - ...emitter.create('root'), - resetRoot, - dispatch, - canGoBack, - getRootState, - })); - - const builderContext = React.useMemo( - () => ({ - addFocusedListener, - addStateGetter, - trackAction, - }), - [addFocusedListener, trackAction, addStateGetter] - ); - - const context = React.useMemo( - () => ({ - state, - performTransaction, - getState, - setState, - }), - [getState, performTransaction, setState, state] - ); - - React.useEffect(() => { - if (process.env.NODE_ENV !== 'production') { - if ( - state !== undefined && - !isSerializable(state) && - !hasWarnedForSerialization - ) { - hasWarnedForSerialization = true; - - console.warn( - "We found non-serializable values in the navigation state, which can break usage such as persisting and restoring state. This might happen if you passed non-serializable values such as function, class instances etc. in params. If you need to use functions in your options, you can use 'navigation.setOptions' instead." - ); - } - } - - emitter.emit({ - type: 'state', - data: { state }, - }); - - if (skipTrackingRef.current) { - skipTrackingRef.current = false; - } else { - trackState(getRootState); - } - - navigationStateRef.current = state; - transactionStateRef.current = null; - - if (!isFirstMountRef.current && onStateChange) { - onStateChange(getRootState()); - } - - isFirstMountRef.current = false; - }, [state, onStateChange, trackState, getRootState, emitter]); - - return ( - - - {children} - - - ); -}); - -export default Container; diff --git a/packages/core/src/SceneView.tsx b/packages/core/src/SceneView.tsx index 50801d36..5453e270 100644 --- a/packages/core/src/SceneView.tsx +++ b/packages/core/src/SceneView.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { NavigationStateContext } from './NavigationContainer'; +import { NavigationStateContext } from './BaseNavigationContainer'; import NavigationContext from './NavigationContext'; import NavigationRouteContext from './NavigationRouteContext'; import StaticContainer from './StaticContainer'; diff --git a/packages/core/src/__tests__/NavigationContainer.test.tsx b/packages/core/src/__tests__/BaseNavigationContainer.test.tsx similarity index 93% rename from packages/core/src/__tests__/NavigationContainer.test.tsx rename to packages/core/src/__tests__/BaseNavigationContainer.test.tsx index 8f9ef281..f6b5adb4 100644 --- a/packages/core/src/__tests__/NavigationContainer.test.tsx +++ b/packages/core/src/__tests__/BaseNavigationContainer.test.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import { act, render } from 'react-native-testing-library'; -import NavigationContainer, { +import BaseNavigationContainer, { NavigationStateContext, -} from '../NavigationContainer'; +} from '../BaseNavigationContainer'; import MockRouter, { MockActions } from './__fixtures__/MockRouter'; import useNavigationBuilder from '../useNavigationBuilder'; import Screen from '../Screen'; @@ -85,9 +85,9 @@ it('throws when setState is called outside performTransaction', () => { }; const element = ( - + - + ); expect(() => render(element).update(element)).toThrowError( @@ -112,9 +112,9 @@ it('throws when nesting performTransaction', () => { }; const element = ( - + - + ); expect(() => render(element).update(element)).toThrowError( @@ -125,11 +125,11 @@ it('throws when nesting performTransaction', () => { it('throws when nesting containers', () => { expect(() => render( - - + + - - + + ) ).toThrowError( "Looks like you have nested a 'NavigationContainer' inside another." @@ -137,11 +137,11 @@ it('throws when nesting containers', () => { expect(() => render( - - + + - - + + ) ).not.toThrowError( "Looks like you have nested a 'NavigationContainer' inside another." @@ -223,7 +223,7 @@ it('handle dispatching with ref', () => { }; const element = ( - { )} - + ); render(element).update(element); @@ -301,7 +301,7 @@ it('handle resetting state with ref', () => { const onStateChange = jest.fn(); const element = ( - + {() => null} @@ -322,7 +322,7 @@ it('handle resetting state with ref', () => { )} - + ); render(element).update(element); @@ -389,7 +389,7 @@ it('handles getRootState', () => { const ref = React.createRef(); const element = ( - + {() => ( @@ -401,7 +401,7 @@ it('handles getRootState', () => { {() => null} - + ); render(element); @@ -450,13 +450,13 @@ it('emits state events when the state changes', () => { const ref = React.createRef(); const element = ( - + {() => null} {() => null} {() => null} - + ); render(element).update(element); diff --git a/packages/core/src/__tests__/CommonActions.test.tsx b/packages/core/src/__tests__/CommonActions.test.tsx index 1bf3e57b..9967e2d9 100644 --- a/packages/core/src/__tests__/CommonActions.test.tsx +++ b/packages/core/src/__tests__/CommonActions.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { render } from 'react-native-testing-library'; import Screen from '../Screen'; -import NavigationContainer from '../NavigationContainer'; +import BaseNavigationContainer from '../BaseNavigationContainer'; import useNavigationBuilder from '../useNavigationBuilder'; import MockRouter, { MockRouterKey } from './__fixtures__/MockRouter'; @@ -26,7 +26,7 @@ it('throws if NAVIGATE dispatched neither key nor name', () => { const onStateChange = jest.fn(); const element = ( - + { initialParams={{ count: 10 }} /> - + ); expect(() => render(element).update(element)).toThrowError( diff --git a/packages/core/src/__tests__/index.test.tsx b/packages/core/src/__tests__/index.test.tsx index a18e8aee..88a4d8f1 100644 --- a/packages/core/src/__tests__/index.test.tsx +++ b/packages/core/src/__tests__/index.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { render, act } from 'react-native-testing-library'; import Screen from '../Screen'; -import NavigationContainer from '../NavigationContainer'; +import BaseNavigationContainer from '../BaseNavigationContainer'; import useNavigationBuilder from '../useNavigationBuilder'; import useNavigation from '../useNavigation'; import MockRouter, { MockRouterKey } from './__fixtures__/MockRouter'; @@ -28,7 +28,7 @@ it('initializes state for a navigator on navigation', () => { const onStateChange = jest.fn(); const element = ( - + { )} - + ); render(element).update(element); @@ -75,11 +75,11 @@ it("doesn't crash when initialState is null", () => { const element = ( // @ts-ignore - + - + ); expect(() => render(element)).not.toThrowError(); @@ -112,7 +112,7 @@ it('rehydrates state for a navigator on navigation', () => { const onStateChange = jest.fn(); const element = ( - @@ -120,7 +120,7 @@ it('rehydrates state for a navigator on navigation', () => { - + ); render(element).update(element); @@ -166,7 +166,7 @@ it("doesn't rehydrate state if the type of state didn't match router", () => { const onStateChange = jest.fn(); const element = ( - @@ -178,7 +178,7 @@ it("doesn't rehydrate state if the type of state didn't match router", () => { /> - + ); render(element).update(element); @@ -219,7 +219,7 @@ it('initializes state for nested screens in React.Fragment', () => { const onStateChange = jest.fn(); const element = ( - + @@ -227,7 +227,7 @@ it('initializes state for nested screens in React.Fragment', () => { - + ); render(element).update(element); @@ -266,7 +266,7 @@ it('initializes state for nested navigator on navigation', () => { const onStateChange = jest.fn(); const element = ( - + @@ -278,7 +278,7 @@ it('initializes state for nested navigator on navigation', () => { )} - + ); render(element).update(element); @@ -328,12 +328,12 @@ it("doesn't update state if nothing changed", () => { const onStateChange = jest.fn(); render( - + - + ); expect(onStateChange).toBeCalledTimes(0); @@ -360,12 +360,12 @@ it("doesn't update state if action wasn't handled", () => { const spy = jest.spyOn(console, 'error').mockImplementation(); render( - + - + ); expect(onStateChange).toBeCalledTimes(0); @@ -396,12 +396,12 @@ it('cleans up state when the navigator unmounts', () => { const onStateChange = jest.fn(); const element = ( - + - + ); const root = render(element); @@ -422,7 +422,7 @@ it('cleans up state when the navigator unmounts', () => { }); root.update( - + ); expect(onStateChange).toBeCalledTimes(2); @@ -454,12 +454,12 @@ it('allows state updates by dispatching a function returning an action', () => { const onStateChange = jest.fn(); const element = ( - + - + ); render(element).update(element); @@ -496,12 +496,12 @@ it('updates route params with setParams', () => { const onStateChange = jest.fn(); render( - + - + ); act(() => setParams({ username: 'alice' })); @@ -556,7 +556,7 @@ it('updates route params with setParams applied to parent', () => { const onStateChange = jest.fn(); render( - + {() => ( @@ -567,7 +567,7 @@ it('updates route params with setParams applied to parent', () => { - + ); act(() => setParams({ username: 'alice' })); @@ -634,22 +634,22 @@ it('handles change in route names', () => { const onStateChange = jest.fn(); const root = render( - + - + ); root.update( - + - + ); expect(onStateChange).toBeCalledWith({ @@ -677,7 +677,7 @@ it('navigates to nested child in a navigator', () => { const navigation = React.createRef(); const element = render( - + {() => ( @@ -704,7 +704,7 @@ it('navigates to nested child in a navigator', () => { )} - + ); expect(element).toMatchInlineSnapshot(`"[foo-a, undefined]"`); @@ -752,11 +752,11 @@ it('gives access to internal state', () => { }; const root = ( - + - + ); render(root).update(root); @@ -778,9 +778,9 @@ it("throws if navigator doesn't have any screens", () => { }; const element = ( - + - + ); expect(() => render(element).update(element)).toThrowError( @@ -812,14 +812,14 @@ it('throws if multiple navigators rendered under one container', () => { }; const element = ( - + - + ); expect(() => render(element).update(element)).toThrowError( @@ -836,12 +836,12 @@ it('throws when Screen is not the direct children', () => { const Bar = () => null; const element = ( - + - + ); expect(() => render(element).update(element)).toThrowError( @@ -856,12 +856,12 @@ it('throws when a React Element is not the direct children', () => { }; const element = ( - + Hello world - + ); expect(() => render(element).update(element)).toThrowError( @@ -877,7 +877,7 @@ it("doesn't throw when direct children is Screen or empty element", () => { }; render( - + {null} @@ -885,7 +885,7 @@ it("doesn't throw when direct children is Screen or empty element", () => { {false} {true} - + ); }); @@ -896,13 +896,13 @@ it('throws when multiple screens with same name are defined', () => { }; const element = ( - + - + ); expect(() => render(element).update(element)).toThrowError( @@ -917,20 +917,20 @@ it('switches rendered navigators', () => { }; const root = render( - + - + ); expect(() => root.update( - + - + ) ).not.toThrowError( 'Another navigator is already registered for this container.' @@ -944,11 +944,11 @@ it('throws if no name is passed to Screen', () => { }; const element = ( - + - + ); expect(() => render(element).update(element)).toThrowError( @@ -963,11 +963,11 @@ it('throws if invalid name is passed to Screen', () => { }; const element = ( - + - + ); expect(() => render(element).update(element)).toThrowError( @@ -982,13 +982,13 @@ it('throws if both children and component are passed', () => { }; const element = ( - + {jest.fn()} - + ); expect(() => render(element).update(element)).toThrowError( @@ -1003,11 +1003,11 @@ it('throws descriptive error for undefined screen component', () => { }; const element = ( - + - + ); expect(() => render(element).update(element)).toThrowError( @@ -1022,11 +1022,11 @@ it('throws descriptive error for invalid screen component', () => { }; const element = ( - + - + ); expect(() => render(element).update(element)).toThrowError( @@ -1041,11 +1041,11 @@ it('throws descriptive error for invalid children', () => { }; const element = ( - + {[] as any} - + ); expect(() => render(element).update(element)).toThrowError( @@ -1060,13 +1060,13 @@ it("doesn't throw if children is null", () => { }; const element = ( - + {null as any} - + ); expect(() => render(element).update(element)).not.toThrowError(); diff --git a/packages/core/src/__tests__/useDescriptors.test.tsx b/packages/core/src/__tests__/useDescriptors.test.tsx index e22ccbfe..4107b019 100644 --- a/packages/core/src/__tests__/useDescriptors.test.tsx +++ b/packages/core/src/__tests__/useDescriptors.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { render, act } from 'react-native-testing-library'; import useNavigationBuilder from '../useNavigationBuilder'; -import NavigationContainer from '../NavigationContainer'; +import BaseNavigationContainer from '../BaseNavigationContainer'; import Screen from '../Screen'; import MockRouter, { MockActions, @@ -34,7 +34,7 @@ it('sets options with options prop as an object', () => { const TestScreen = (): any => 'Test screen'; const root = render( - + { /> - + ); expect(root).toMatchInlineSnapshot(` @@ -79,7 +79,7 @@ it('sets options with options prop as a fuction', () => { const TestScreen = (): any => 'Test screen'; const root = render( - + { /> - + ); expect(root).toMatchInlineSnapshot(` @@ -134,12 +134,12 @@ it('sets options with screenOptions prop as an object', () => { const TestScreenB = (): any => 'Test screen B'; const root = render( - + - + ); expect(root).toMatchInlineSnapshot(` @@ -194,7 +194,7 @@ it('sets options with screenOptions prop as a fuction', () => { const TestScreenB = (): any => 'Test screen B'; const root = render( - + ({ title: `${route.name}: ${route.params.author || route.params.fruit}`, @@ -211,7 +211,7 @@ it('sets options with screenOptions prop as a fuction', () => { initialParams={{ fruit: 'Apple' }} /> - + ); expect(root).toMatchInlineSnapshot(` @@ -266,14 +266,14 @@ it('sets initial options with setOptions', () => { }; const root = render( - + {props => } - + ); expect(root).toMatchInlineSnapshot(` @@ -331,14 +331,14 @@ it('updates options with setOptions', () => { }; const element = ( - + {props => } - + ); const root = render(element); @@ -396,7 +396,7 @@ it("returns correct value for canGoBack when it's not overridden", () => { }; const root = ( - + { /> - + ); render(root).update(root); @@ -452,11 +452,11 @@ it(`returns false for canGoBack when current router doesn't handle GO_BACK`, () }; const root = ( - + - + ); render(root).update(root); @@ -513,7 +513,7 @@ it('returns true for canGoBack when current router handles GO_BACK', () => { }; const root = ( - + {() => ( @@ -523,7 +523,7 @@ it('returns true for canGoBack when current router handles GO_BACK', () => { )} - + ); render(root).update(root); @@ -580,7 +580,7 @@ it('returns true for canGoBack when parent router handles GO_BACK', () => { }; const root = ( - + {() => ( @@ -597,7 +597,7 @@ it('returns true for canGoBack when parent router handles GO_BACK', () => { )} - + ); render(root).update(root); diff --git a/packages/core/src/__tests__/useEventEmitter.test.tsx b/packages/core/src/__tests__/useEventEmitter.test.tsx index c6a3124c..922f703a 100644 --- a/packages/core/src/__tests__/useEventEmitter.test.tsx +++ b/packages/core/src/__tests__/useEventEmitter.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { render, act } from 'react-native-testing-library'; import useNavigationBuilder from '../useNavigationBuilder'; -import NavigationContainer from '../NavigationContainer'; +import BaseNavigationContainer from '../BaseNavigationContainer'; import Screen from '../Screen'; import MockRouter from './__fixtures__/MockRouter'; import { Router, NavigationState } from '../types'; @@ -47,7 +47,7 @@ it('fires focus and blur events in root navigator', () => { const navigation = React.createRef(); const element = ( - + { component={createComponent(fourthFocusCallback, fourthBlurCallback)} /> - + ); render(element); @@ -139,7 +139,7 @@ it('fires focus and blur events in nested navigator', () => { const child = React.createRef(); const element = ( - + { )} - + ); render(element); @@ -307,12 +307,12 @@ it('fires blur event when a route is removed with a delay', async () => { const navigation = React.createRef(); const element = ( - + - + ); render(element); @@ -363,13 +363,13 @@ it('fires custom events', () => { const ref = React.createRef(); const element = ( - + - + ); render(element); @@ -444,11 +444,11 @@ it('has option to prevent default', () => { const ref = React.createRef(); const element = ( - + - + ); render(element); diff --git a/packages/core/src/__tests__/useFocusEffect.test.tsx b/packages/core/src/__tests__/useFocusEffect.test.tsx index bd080715..845dbf1d 100644 --- a/packages/core/src/__tests__/useFocusEffect.test.tsx +++ b/packages/core/src/__tests__/useFocusEffect.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { render, act } from 'react-native-testing-library'; import useNavigationBuilder from '../useNavigationBuilder'; import useFocusEffect from '../useFocusEffect'; -import NavigationContainer from '../NavigationContainer'; +import BaseNavigationContainer from '../BaseNavigationContainer'; import Screen from '../Screen'; import MockRouter from './__fixtures__/MockRouter'; @@ -31,13 +31,13 @@ it('runs focus effect on focus change', () => { const navigation = React.createRef(); const element = ( - + {() => null} {() => null} - + ); render(element); @@ -84,12 +84,12 @@ it('runs focus effect on deps change', () => { }; const App = ({ count }: { count: number }) => ( - + {() => } {() => null} - + ); const root = render(); @@ -135,13 +135,13 @@ it('runs focus effect when initial state is given', () => { const navigation = React.createRef(); const element = ( - + {() => null} {() => null} - + ); render(element); @@ -178,13 +178,13 @@ it('runs focus effect when only focused route is rendered', () => { const navigation = React.createRef(); const element = ( - + {() => null} {() => null} - + ); render(element); @@ -221,12 +221,12 @@ it('runs cleanup when component is unmounted', () => { const TestB = () => null; const App = ({ mounted }: { mounted: boolean }) => ( - + {() => null} - + ); const root = render(); diff --git a/packages/core/src/__tests__/useIsFocused.test.tsx b/packages/core/src/__tests__/useIsFocused.test.tsx index b5c429ff..80a6a661 100644 --- a/packages/core/src/__tests__/useIsFocused.test.tsx +++ b/packages/core/src/__tests__/useIsFocused.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { render, act } from 'react-native-testing-library'; import useNavigationBuilder from '../useNavigationBuilder'; import useIsFocused from '../useIsFocused'; -import NavigationContainer from '../NavigationContainer'; +import BaseNavigationContainer from '../BaseNavigationContainer'; import Screen from '../Screen'; import MockRouter from './__fixtures__/MockRouter'; @@ -24,13 +24,13 @@ it('renders correct focus state', () => { const navigation = React.createRef(); const root = render( - + {() => null} {() => null} - + ); expect(root).toMatchInlineSnapshot(`"unfocused"`); diff --git a/packages/core/src/__tests__/useNavigation.test.tsx b/packages/core/src/__tests__/useNavigation.test.tsx index 3321c9ab..b3cdecc0 100644 --- a/packages/core/src/__tests__/useNavigation.test.tsx +++ b/packages/core/src/__tests__/useNavigation.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { render } from 'react-native-testing-library'; import useNavigationBuilder from '../useNavigationBuilder'; import useNavigation from '../useNavigation'; -import NavigationContainer from '../NavigationContainer'; +import BaseNavigationContainer from '../BaseNavigationContainer'; import Screen from '../Screen'; import MockRouter from './__fixtures__/MockRouter'; @@ -24,11 +24,11 @@ it('gets navigation prop from context', () => { }; render( - + - + ); }); @@ -50,7 +50,7 @@ it("gets navigation's parent from context", () => { }; render( - + {() => ( @@ -60,7 +60,7 @@ it("gets navigation's parent from context", () => { )} - + ); }); @@ -86,7 +86,7 @@ it("gets navigation's parent's parent from context", () => { }; render( - + {() => ( @@ -102,7 +102,7 @@ it("gets navigation's parent's parent from context", () => { )} - + ); }); diff --git a/packages/core/src/__tests__/useNavigationState.test.tsx b/packages/core/src/__tests__/useNavigationState.test.tsx index 67ef8e8b..abee7882 100644 --- a/packages/core/src/__tests__/useNavigationState.test.tsx +++ b/packages/core/src/__tests__/useNavigationState.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { render, act } from 'react-native-testing-library'; import useNavigationBuilder from '../useNavigationBuilder'; import useNavigationState from '../useNavigationState'; -import NavigationContainer from '../NavigationContainer'; +import BaseNavigationContainer from '../BaseNavigationContainer'; import Screen from '../Screen'; import MockRouter from './__fixtures__/MockRouter'; import { NavigationState } from '../types'; @@ -27,13 +27,13 @@ it('gets the current navigation state', () => { const navigation = React.createRef(); const element = ( - + {() => null} {() => null} - + ); render(element); @@ -78,13 +78,13 @@ it('gets the current navigation state with selector', () => { const navigation = React.createRef(); const element = ( - + {() => null} {() => null} - + ); render(element); @@ -133,13 +133,13 @@ it('gets the correct value if selector changes', () => { const App = ({ selector }: { selector: (state: NavigationState) => any }) => { return ( - + {() => null} {() => null} - + ); }; diff --git a/packages/core/src/__tests__/useOnAction.test.tsx b/packages/core/src/__tests__/useOnAction.test.tsx index da072787..726215c7 100644 --- a/packages/core/src/__tests__/useOnAction.test.tsx +++ b/packages/core/src/__tests__/useOnAction.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { render } from 'react-native-testing-library'; import useNavigationBuilder from '../useNavigationBuilder'; -import NavigationContainer from '../NavigationContainer'; +import BaseNavigationContainer from '../BaseNavigationContainer'; import Screen from '../Screen'; import MockRouter, { MockActions, @@ -58,7 +58,7 @@ it("lets parent handle the action if child didn't", () => { const onStateChange = jest.fn(); render( - + {() => null} {() => null} @@ -70,7 +70,7 @@ it("lets parent handle the action if child didn't", () => { )} - + ); expect(onStateChange).toBeCalledTimes(1); @@ -171,7 +171,7 @@ it("lets children handle the action if parent didn't", () => { }; const element = ( - @@ -187,7 +187,7 @@ it("lets children handle the action if parent didn't", () => { )} - + ); render(element).update(element); @@ -284,7 +284,7 @@ it("action doesn't bubble if target is specified", () => { const onStateChange = jest.fn(); const element = ( - + {() => null} @@ -297,7 +297,7 @@ it("action doesn't bubble if target is specified", () => { )} - + ); render(element).update(element); @@ -349,7 +349,7 @@ it('logs error if no navigator handled the action', () => { }; const element = ( - + {() => null} @@ -362,7 +362,7 @@ it('logs error if no navigator handled the action', () => { )} - + ); const spy = jest.spyOn(console, 'error').mockImplementation(); diff --git a/packages/core/src/__tests__/useRoute.test.tsx b/packages/core/src/__tests__/useRoute.test.tsx index 4743aefe..1a43f98e 100644 --- a/packages/core/src/__tests__/useRoute.test.tsx +++ b/packages/core/src/__tests__/useRoute.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { render } from 'react-native-testing-library'; import useNavigationBuilder from '../useNavigationBuilder'; import useRoute from '../useRoute'; -import NavigationContainer from '../NavigationContainer'; +import BaseNavigationContainer from '../BaseNavigationContainer'; import Screen from '../Screen'; import MockRouter from './__fixtures__/MockRouter'; import { RouteProp } from '../types'; @@ -25,10 +25,10 @@ it('gets route prop from context', () => { }; render( - + - + ); }); diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index c8ab7d59..88274e3f 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -3,7 +3,7 @@ import * as CommonActions from './CommonActions'; export { CommonActions }; export { default as BaseRouter } from './BaseRouter'; -export { default as NavigationContainer } from './NavigationContainer'; +export { default as BaseNavigationContainer } from './BaseNavigationContainer'; export { default as createNavigatorFactory } from './createNavigatorFactory'; export { default as NavigationContext } from './NavigationContext'; diff --git a/packages/core/src/useNavigationBuilder.tsx b/packages/core/src/useNavigationBuilder.tsx index c54a6cce..604a4d8b 100644 --- a/packages/core/src/useNavigationBuilder.tsx +++ b/packages/core/src/useNavigationBuilder.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { isValidElementType } from 'react-is'; -import { NavigationStateContext } from './NavigationContainer'; +import { NavigationStateContext } from './BaseNavigationContainer'; import NavigationRouteContext from './NavigationRouteContext'; import Screen from './Screen'; import { navigate } from './CommonActions'; diff --git a/packages/core/src/useNavigationHelpers.tsx b/packages/core/src/useNavigationHelpers.tsx index cdba2965..e0b23691 100644 --- a/packages/core/src/useNavigationHelpers.tsx +++ b/packages/core/src/useNavigationHelpers.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import * as CommonActions from './CommonActions'; import NavigationContext from './NavigationContext'; -import { NavigationStateContext } from './NavigationContainer'; +import { NavigationStateContext } from './BaseNavigationContainer'; import { NavigationEventEmitter } from './useEventEmitter'; import { NavigationHelpers, diff --git a/packages/native/src/NavigationContainer.tsx b/packages/native/src/NavigationContainer.tsx new file mode 100644 index 00000000..436e9824 --- /dev/null +++ b/packages/native/src/NavigationContainer.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import { + BaseNavigationContainer, + NavigationContainerProps, + NavigationContainerRef, +} from '@react-navigation/core'; +import ThemeProvider from './theming/ThemeProvider'; +import DefaultTheme from './theming/DefaultTheme'; +import useBackButton from './useBackButton'; +import { Theme } from './types'; + +type Props = NavigationContainerProps & { + theme?: Theme; +}; + +/** + * Container component which holds the navigation state + * designed for mobile apps. + * This should be rendered at the root wrapping the whole app. + * + * @param props.initialState Initial state object for the navigation tree. + * @param props.onStateChange Callback which is called with the latest navigation state when it changes. + * @param props.theme Theme object for the navigators. + * @param props.children Child elements to render the content. + * @param props.ref Ref object which refers to the navigation object containing helper methods. + */ +const NavigationContainer = React.forwardRef(function NavigationContainer( + { theme = DefaultTheme, ...rest }: Props, + ref: React.Ref +) { + const refContainer = React.useRef(null); + + useBackButton(refContainer); + + React.useImperativeHandle(ref, () => refContainer.current); + + return ( + + + + ); +}); + +export default NavigationContainer; diff --git a/packages/native/src/NavigationNativeContainer.tsx b/packages/native/src/NavigationNativeContainer.tsx index d40a75c2..612261e5 100644 --- a/packages/native/src/NavigationNativeContainer.tsx +++ b/packages/native/src/NavigationNativeContainer.tsx @@ -1,44 +1,5 @@ -import * as React from 'react'; -import { - NavigationContainer, - NavigationContainerProps, - NavigationContainerRef, -} from '@react-navigation/core'; -import ThemeProvider from './theming/ThemeProvider'; -import DefaultTheme from './theming/DefaultTheme'; -import useBackButton from './useBackButton'; -import { Theme } from './types'; - -type Props = NavigationContainerProps & { - theme?: Theme; -}; - -/** - * Container component which holds the navigation state - * designed for mobile apps. - * This should be rendered at the root wrapping the whole app. - * - * @param props.initialState Initial state object for the navigation tree. - * @param props.onStateChange Callback which is called with the latest navigation state when it changes. - * @param props.theme Theme object for the navigators. - * @param props.children Child elements to render the content. - * @param props.ref Ref object which refers to the navigation object containing helper methods. - */ -const NavigationNativeContainer = React.forwardRef(function NativeContainer( - { theme = DefaultTheme, ...rest }: Props, - ref: React.Ref -) { - const refContainer = React.useRef(null); - - useBackButton(refContainer); - - React.useImperativeHandle(ref, () => refContainer.current); - - return ( - - - +export default function() { + throw new Error( + "'NavigationNativeContainer' has been renamed to 'NavigationContainer" ); -}); - -export default NavigationNativeContainer; +} diff --git a/packages/native/src/index.tsx b/packages/native/src/index.tsx index 8d52375d..852b5485 100644 --- a/packages/native/src/index.tsx +++ b/packages/native/src/index.tsx @@ -1,5 +1,6 @@ export * from '@react-navigation/core'; +export { default as NavigationContainer } from './NavigationContainer'; export { default as NavigationNativeContainer } from './NavigationNativeContainer'; export { default as useBackButton } from './useBackButton';