From 0ecd112ec9786a26261ada3d33ef44dc1ec84da0 Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Sat, 1 May 2021 23:51:10 +0200 Subject: [PATCH] feat: add helper and hook for container ref --- example/src/index.tsx | 4 +- packages/core/src/BaseNavigationContainer.tsx | 20 ++--- .../BaseNavigationContainer.test.tsx | 25 +++--- packages/core/src/__tests__/index.test.tsx | 88 +++++++++---------- .../core/src/__tests__/useOnAction.test.tsx | 13 +-- .../core/src/createNavigationContainerRef.tsx | 47 ++++++++++ packages/core/src/index.tsx | 3 + packages/core/src/types.tsx | 14 ++- .../core/src/useNavigationContainerRef.tsx | 18 ++++ .../src/useReduxDevToolsExtension.tsx | 3 +- packages/native/src/NavigationContainer.tsx | 7 +- .../__tests__/NavigationContainer.test.tsx | 5 +- .../native/src/__tests__/useLinking.test.tsx | 7 +- packages/native/src/useBackButton.tsx | 7 +- packages/native/src/useDocumentTitle.tsx | 7 +- packages/native/src/useLinking.native.tsx | 3 +- packages/native/src/useLinking.tsx | 3 +- 17 files changed, 179 insertions(+), 95 deletions(-) create mode 100644 packages/core/src/createNavigationContainerRef.tsx create mode 100644 packages/core/src/useNavigationContainerRef.tsx diff --git a/example/src/index.tsx b/example/src/index.tsx index 42a30276..6eb54f73 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -28,7 +28,7 @@ import { DefaultTheme, DarkTheme, PathConfigMap, - NavigationContainerRef, + useNavigationContainerRef, } from '@react-navigation/native'; import { createDrawerNavigator } from '@react-navigation/drawer'; import { @@ -193,7 +193,7 @@ export default function App() { return () => Dimensions.removeEventListener('change', onDimensionsChange); }, []); - const navigationRef = React.useRef(null); + const navigationRef = useNavigationContainerRef(); useReduxDevToolsExtension(navigationRef); diff --git a/packages/core/src/BaseNavigationContainer.tsx b/packages/core/src/BaseNavigationContainer.tsx index 92ea4e99..a2356d1d 100644 --- a/packages/core/src/BaseNavigationContainer.tsx +++ b/packages/core/src/BaseNavigationContainer.tsx @@ -6,6 +6,7 @@ import { InitialState, PartialState, NavigationAction, + ParamListBase, } from '@react-navigation/routers'; import EnsureSingleNavigator from './EnsureSingleNavigator'; import UnhandledActionContext from './UnhandledActionContext'; @@ -22,6 +23,7 @@ import useSyncState from './useSyncState'; import checkSerializable from './checkSerializable'; import checkDuplicateRouteNames from './checkDuplicateRouteNames'; import findFocusedRoute from './findFocusedRoute'; +import { NOT_INITIALIZED_ERROR } from './createNavigationContainerRef'; import type { NavigationContainerEventMap, NavigationContainerRef, @@ -30,9 +32,6 @@ import type { type State = NavigationState | PartialState | undefined; -const NOT_INITIALIZED_ERROR = - "The 'navigation' object hasn't been initialized yet. This might happen if you don't have a navigator mounted, or if the navigator hasn't finished mounting. See https://reactnavigation.org/docs/navigating-without-navigation-prop#handling-initialization for more details."; - const serializableWarnings: string[] = []; const duplicateNameWarnings: string[] = []; @@ -103,7 +102,7 @@ const BaseNavigationContainer = React.forwardRef( independent, children, }: NavigationContainerProps, - ref?: React.Ref + ref?: React.Ref> ) { const parent = React.useContext(NavigationStateContext); @@ -202,16 +201,10 @@ const BaseNavigationContainer = React.forwardRef( const { addOptionsGetter, getCurrentOptions } = useOptionsGetters({}); React.useImperativeHandle(ref, () => ({ - ...(Object.keys( - CommonActions - ) as (keyof typeof CommonActions)[]).reduce((acc, name) => { + ...Object.keys(CommonActions).reduce((acc, name) => { acc[name] = (...args: any[]) => - dispatch( - CommonActions[name]( - // @ts-expect-error: we can't know the type statically - ...args - ) - ); + // @ts-expect-error: this is ok + dispatch(CommonActions[name](...args)); return acc; }, {}), ...emitter.create('root'), @@ -223,6 +216,7 @@ const BaseNavigationContainer = React.forwardRef( getParent: () => undefined, getCurrentRoute, getCurrentOptions, + isReady: () => listeners.focus[0] != null, })); const onDispatchAction = React.useCallback( diff --git a/packages/core/src/__tests__/BaseNavigationContainer.test.tsx b/packages/core/src/__tests__/BaseNavigationContainer.test.tsx index 65a978b2..76c8e240 100644 --- a/packages/core/src/__tests__/BaseNavigationContainer.test.tsx +++ b/packages/core/src/__tests__/BaseNavigationContainer.test.tsx @@ -3,16 +3,17 @@ import { act, render } from '@testing-library/react-native'; import { DefaultRouterOptions, NavigationState, + ParamListBase, Router, StackRouter, TabRouter, } from '@react-navigation/routers'; import BaseNavigationContainer from '../BaseNavigationContainer'; import NavigationStateContext from '../NavigationStateContext'; +import createNavigationContainerRef from '../createNavigationContainerRef'; import MockRouter, { MockActions } from './__fixtures__/MockRouter'; import useNavigationBuilder from '../useNavigationBuilder'; import Screen from '../Screen'; -import type { NavigationContainerRef } from '../types'; it('throws when getState is accessed without a container', () => { expect.assertions(1); @@ -128,7 +129,7 @@ it('handle dispatching with ref', () => { ); }; - const ref = React.createRef(); + const ref = createNavigationContainerRef(); const onStateChange = jest.fn(); @@ -226,7 +227,7 @@ it('handle resetting state with ref', () => { ); }; - const ref = React.createRef(); + const ref = createNavigationContainerRef(); const onStateChange = jest.fn(); @@ -316,7 +317,7 @@ it('handles getRootState', () => { return descriptors[state.routes[state.index].key].render(); }; - const ref = React.createRef(); + const ref = createNavigationContainerRef(); const element = ( @@ -378,7 +379,7 @@ it('emits state events when the state changes', () => { ); }; - const ref = React.createRef(); + const ref = createNavigationContainerRef(); const element = ( @@ -448,7 +449,7 @@ it('emits state events when new navigator mounts', () => { ); }; - const ref = React.createRef(); + const ref = createNavigationContainerRef(); const NestedNavigator = () => { const [isRendered, setIsRendered] = React.useState(false); @@ -537,7 +538,7 @@ it('emits option events when options change with tab router', () => { ); }; - const ref = React.createRef(); + const ref = createNavigationContainerRef(); const element = ( @@ -611,7 +612,7 @@ it('emits option events when options change with stack router', () => { ); }; - const ref = React.createRef(); + const ref = createNavigationContainerRef(); const element = ( @@ -677,7 +678,7 @@ it('emits option events when options change with stack router', () => { it('throws if there is no navigator rendered', () => { expect.assertions(1); - const ref = React.createRef(); + const ref = createNavigationContainerRef(); const element = ; @@ -697,7 +698,7 @@ it('throws if there is no navigator rendered', () => { it("throws if the ref hasn't finished initializing", () => { expect.assertions(1); - const ref = React.createRef(); + const ref = createNavigationContainerRef(); const TestNavigator = (props: any) => { const { state, descriptors } = useNavigationBuilder(MockRouter, props); @@ -733,7 +734,7 @@ it("throws if the ref hasn't finished initializing", () => { }); it('invokes the unhandled action listener with the unhandled action', () => { - const ref = React.createRef(); + const ref = createNavigationContainerRef(); const fn = jest.fn(); const TestNavigator = (props: any) => { @@ -779,7 +780,7 @@ it('works with state change events in independent nested container', () => { ); }; - const ref = React.createRef(); + const ref = createNavigationContainerRef(); const onStateChange = jest.fn(); diff --git a/packages/core/src/__tests__/index.test.tsx b/packages/core/src/__tests__/index.test.tsx index dbb0c9e0..79273013 100644 --- a/packages/core/src/__tests__/index.test.tsx +++ b/packages/core/src/__tests__/index.test.tsx @@ -1,12 +1,12 @@ import * as React from 'react'; import { render, act } from '@testing-library/react-native'; -import type { NavigationState } from '@react-navigation/routers'; +import type { NavigationState, ParamListBase } from '@react-navigation/routers'; import Screen from '../Screen'; import BaseNavigationContainer from '../BaseNavigationContainer'; import useNavigationBuilder from '../useNavigationBuilder'; +import createNavigationContainerRef from '../createNavigationContainerRef'; import useNavigation from '../useNavigation'; import MockRouter, { MockRouterKey } from './__fixtures__/MockRouter'; -import type { NavigationContainerRef } from '../types'; beforeEach(() => (MockRouterKey.current = 0)); @@ -679,7 +679,7 @@ it('navigates to nested child in a navigator', () => { const onStateChange = jest.fn(); - const navigation = React.createRef(); + const navigation = createNavigationContainerRef(); const element = render( @@ -715,7 +715,7 @@ it('navigates to nested child in a navigator', () => { expect(element).toMatchInlineSnapshot(`"[foo-a, undefined]"`); act(() => - navigation.current?.navigate('bar', { + navigation.navigate('bar', { screen: 'bar-b', params: { test: 42 }, }) @@ -726,7 +726,7 @@ it('navigates to nested child in a navigator', () => { ); act(() => - navigation.current?.navigate('bar', { + navigation.navigate('bar', { screen: 'bar-a', params: { whoa: 'test' }, }) @@ -736,15 +736,15 @@ it('navigates to nested child in a navigator', () => { `"[bar-a, {\\"lol\\":\\"why\\",\\"whoa\\":\\"test\\"}]"` ); - act(() => navigation.current?.navigate('bar', { screen: 'bar-b' })); + act(() => navigation.navigate('bar', { screen: 'bar-b' })); - act(() => navigation.current?.goBack()); + act(() => navigation.goBack()); expect(element).toMatchInlineSnapshot( `"[bar-a, {\\"lol\\":\\"why\\",\\"whoa\\":\\"test\\"}]"` ); - act(() => navigation.current?.navigate('bar', { screen: 'bar-b' })); + act(() => navigation.navigate('bar', { screen: 'bar-b' })); expect(element).toMatchInlineSnapshot( `"[bar-b, {\\"some\\":\\"stuff\\",\\"test\\":42,\\"whoa\\":\\"test\\"}]"` @@ -799,7 +799,7 @@ it('navigates to nested child in a navigator with initial: false', () => { const onStateChange = jest.fn(); - const navigation = React.createRef(); + const navigation = createNavigationContainerRef(); const first = render( @@ -833,7 +833,7 @@ it('navigates to nested child in a navigator with initial: false', () => { ); expect(first).toMatchInlineSnapshot(`"[foo-a, undefined]"`); - expect(navigation.current?.getRootState()).toEqual({ + expect(navigation.getRootState()).toEqual({ index: 0, key: '0', routeNames: ['foo', 'bar'], @@ -866,7 +866,7 @@ it('navigates to nested child in a navigator with initial: false', () => { }); act(() => - navigation.current?.navigate('bar', { + navigation.navigate('bar', { screen: 'bar-b', params: { test: 42 }, }) @@ -876,7 +876,7 @@ it('navigates to nested child in a navigator with initial: false', () => { `"[bar-b, {\\"some\\":\\"stuff\\",\\"test\\":42}]"` ); - expect(navigation.current?.getRootState()).toEqual({ + expect(navigation.getRootState()).toEqual({ index: 2, key: '0', routeNames: ['foo', 'bar'], @@ -944,7 +944,7 @@ it('navigates to nested child in a navigator with initial: false', () => { ); expect(second).toMatchInlineSnapshot(`"[foo-a, undefined]"`); - expect(navigation.current?.getRootState()).toEqual({ + expect(navigation.getRootState()).toEqual({ index: 0, key: '4', routeNames: ['foo', 'bar'], @@ -971,7 +971,7 @@ it('navigates to nested child in a navigator with initial: false', () => { }); act(() => - navigation.current?.navigate('bar', { + navigation.navigate('bar', { screen: 'bar-b', params: { test: 42 }, initial: false, @@ -980,7 +980,7 @@ it('navigates to nested child in a navigator with initial: false', () => { expect(second).toMatchInlineSnapshot(`"[bar-b, {\\"test\\":42}]"`); - expect(navigation.current?.getRootState()).toEqual({ + expect(navigation.getRootState()).toEqual({ index: 2, key: '4', routeNames: ['foo', 'bar'], @@ -1071,7 +1071,7 @@ it('navigates to nested child in a navigator with initial: false', () => { expect(third).toMatchInlineSnapshot(`"[bar-b, {\\"some\\":\\"stuff\\"}]"`); - expect(navigation.current?.getRootState()).toEqual({ + expect(navigation.getRootState()).toEqual({ index: 1, key: '11', routeNames: ['foo', 'bar'], @@ -1119,7 +1119,7 @@ it('resets state of a nested child in a navigator', () => { const onStateChange = jest.fn(); - const navigation = React.createRef(); + const navigation = createNavigationContainerRef(); const first = render( @@ -1150,7 +1150,7 @@ it('resets state of a nested child in a navigator', () => { expect(first).toMatchInlineSnapshot(`"[foo-a, undefined]"`); - expect(navigation.current?.getRootState()).toEqual({ + expect(navigation.getRootState()).toEqual({ index: 0, key: '0', routeNames: ['foo', 'bar'], @@ -1183,7 +1183,7 @@ it('resets state of a nested child in a navigator', () => { }); act(() => - navigation.current?.navigate('bar', { + navigation.navigate('bar', { state: { routes: [{ name: 'bar-a' }, { name: 'bar-b' }], }, @@ -1192,7 +1192,7 @@ it('resets state of a nested child in a navigator', () => { expect(first).toMatchInlineSnapshot(`"[bar-a, undefined]"`); - expect(navigation.current?.getRootState()).toEqual({ + expect(navigation.getRootState()).toEqual({ index: 1, key: '0', routeNames: ['foo', 'bar'], @@ -1231,7 +1231,7 @@ it('resets state of a nested child in a navigator', () => { }); act(() => - navigation.current?.navigate('bar', { + navigation.navigate('bar', { state: { index: 2, routes: [ @@ -1245,7 +1245,7 @@ it('resets state of a nested child in a navigator', () => { expect(first).toMatchInlineSnapshot(`"[bar-a, {\\"test\\":18}]"`); - expect(navigation.current?.getRootState()).toEqual({ + expect(navigation.getRootState()).toEqual({ index: 1, key: '0', routeNames: ['foo', 'bar'], @@ -1336,7 +1336,7 @@ it('preserves order of screens in state with non-numeric names', () => { return null; }; - const navigation = React.createRef(); + const navigation = createNavigationContainerRef(); const root = ( @@ -1350,11 +1350,7 @@ it('preserves order of screens in state with non-numeric names', () => { render(root); - expect(navigation.current?.getRootState().routeNames).toEqual([ - 'foo', - 'bar', - 'baz', - ]); + expect(navigation.getRootState().routeNames).toEqual(['foo', 'bar', 'baz']); }); it('preserves order of screens in state with numeric names', () => { @@ -1363,7 +1359,7 @@ it('preserves order of screens in state with numeric names', () => { return null; }; - const navigation = React.createRef(); + const navigation = createNavigationContainerRef(); const root = ( @@ -1377,11 +1373,7 @@ it('preserves order of screens in state with numeric names', () => { render(root); - expect(navigation.current?.getRootState().routeNames).toEqual([ - '4', - '7', - '1', - ]); + expect(navigation.getRootState().routeNames).toEqual(['4', '7', '1']); }); it("throws if navigator doesn't have any screens", () => { @@ -1801,7 +1793,7 @@ it('returns currently focused route with getCurrentRoute', () => { const TestScreen = () => null; - const navigation = React.createRef(); + const navigation = createNavigationContainerRef(); const container = ( @@ -1824,7 +1816,7 @@ it('returns currently focused route with getCurrentRoute', () => { render(container).update(container); - expect(navigation.current?.getCurrentRoute()).toEqual({ + expect(navigation.getCurrentRoute()).toEqual({ key: 'bar-a', name: 'bar-a', }); @@ -1839,7 +1831,7 @@ it("returns focused screen's options with getCurrentOptions when focused screen const TestScreen = () => null; - const navigation = React.createRef(); + const navigation = createNavigationContainerRef(); const container = ( @@ -1870,14 +1862,14 @@ it("returns focused screen's options with getCurrentOptions when focused screen render(container).update(container); - expect(navigation.current?.getCurrentOptions()).toEqual({ + expect(navigation.getCurrentOptions()).toEqual({ sample: 'data', sample2: 'data', }); - act(() => navigation.current?.navigate('bar-b')); + act(() => navigation.navigate('bar-b')); - expect(navigation.current?.getCurrentOptions()).toEqual({ + expect(navigation.getCurrentOptions()).toEqual({ sample2: 'data', sample3: 'data', }); @@ -1892,7 +1884,7 @@ it("returns focused screen's options with getCurrentOptions when all screens are const TestScreen = () => null; - const navigation = React.createRef(); + const navigation = createNavigationContainerRef(); const container = ( @@ -1923,14 +1915,14 @@ it("returns focused screen's options with getCurrentOptions when all screens are render(container).update(container); - expect(navigation.current?.getCurrentOptions()).toEqual({ + expect(navigation.getCurrentOptions()).toEqual({ sample: 'data', sample2: 'data', }); - act(() => navigation.current?.navigate('bar-b')); + act(() => navigation.navigate('bar-b')); - expect(navigation.current?.getCurrentOptions()).toEqual({ + expect(navigation.getCurrentOptions()).toEqual({ sample2: 'data', sample3: 'data', }); @@ -1945,7 +1937,7 @@ it('does not throw if while getting current options with no options defined', () const TestScreen = () => null; - const navigation = React.createRef(); + const navigation = createNavigationContainerRef(); const container = ( @@ -1968,11 +1960,11 @@ it('does not throw if while getting current options with no options defined', () render(container).update(container); - expect(navigation.current?.getCurrentOptions()).toEqual({}); + expect(navigation.getCurrentOptions()).toEqual({}); }); it('does not throw if while getting current options with empty container', () => { - const navigation = React.createRef(); + const navigation = createNavigationContainerRef(); const container = ( @@ -1980,5 +1972,5 @@ it('does not throw if while getting current options with empty container', () => render(container).update(container); - expect(navigation.current?.getCurrentOptions()).toEqual(undefined); + expect(navigation.getCurrentOptions()).toEqual(undefined); }); diff --git a/packages/core/src/__tests__/useOnAction.test.tsx b/packages/core/src/__tests__/useOnAction.test.tsx index d28d6c64..a12e1c72 100644 --- a/packages/core/src/__tests__/useOnAction.test.tsx +++ b/packages/core/src/__tests__/useOnAction.test.tsx @@ -5,15 +5,16 @@ import { DefaultRouterOptions, NavigationState, StackRouter, + ParamListBase, } from '@react-navigation/routers'; import useNavigationBuilder from '../useNavigationBuilder'; import BaseNavigationContainer from '../BaseNavigationContainer'; import Screen from '../Screen'; +import createNavigationContainerRef from '../createNavigationContainerRef'; import MockRouter, { MockActions, MockRouterKey, } from './__fixtures__/MockRouter'; -import type { NavigationContainerRef } from '../types'; jest.mock('nanoid/non-secure', () => { const m = { nanoid: () => String(++m.__key), __key: 0 }; @@ -571,7 +572,7 @@ it("prevents removing a screen with 'beforeRemove' event", () => { const onStateChange = jest.fn(); - const ref = React.createRef(); + const ref = createNavigationContainerRef(); const element = ( @@ -706,7 +707,7 @@ it("prevents removing a child screen with 'beforeRemove' event", () => { const onStateChange = jest.fn(); - const ref = React.createRef(); + const ref = createNavigationContainerRef(); const element = ( @@ -867,7 +868,7 @@ it("prevents removing a grand child screen with 'beforeRemove' event", () => { const onStateChange = jest.fn(); - const ref = React.createRef(); + const ref = createNavigationContainerRef(); const element = ( @@ -1065,7 +1066,7 @@ it("prevents removing by multiple screens with 'beforeRemove' event", () => { const onStateChange = jest.fn(); - const ref = React.createRef(); + const ref = createNavigationContainerRef(); const element = ( @@ -1217,7 +1218,7 @@ it("prevents removing a child screen with 'beforeRemove' event with 'resetRoot'" const onStateChange = jest.fn(); - const ref = React.createRef(); + const ref = createNavigationContainerRef(); const element = ( diff --git a/packages/core/src/createNavigationContainerRef.tsx b/packages/core/src/createNavigationContainerRef.tsx new file mode 100644 index 00000000..ff07349b --- /dev/null +++ b/packages/core/src/createNavigationContainerRef.tsx @@ -0,0 +1,47 @@ +import { CommonActions, ParamListBase } from '@react-navigation/routers'; +import type { NavigationContainerRefWithCurrent } from './types'; + +export const NOT_INITIALIZED_ERROR = + "The 'navigation' object hasn't been initialized yet. This might happen if you don't have a navigator mounted, or if the navigator hasn't finished mounting. See https://reactnavigation.org/docs/navigating-without-navigation-prop#handling-initialization for more details."; + +export default function createNavigationContainerRef< + ParamList extends ParamListBase +>(): NavigationContainerRefWithCurrent { + const methods = [ + ...Object.keys(CommonActions), + 'addListener', + 'removeListener', + 'resetRoot', + 'dispatch', + 'canGoBack', + 'getRootState', + 'getState', + 'getParent', + 'getCurrentRoute', + 'getCurrentOptions', + ] as const; + + const ref: NavigationContainerRefWithCurrent = { + ...methods.reduce((acc, name) => { + acc[name] = (...args: any[]) => { + if (ref.current == null) { + console.error(NOT_INITIALIZED_ERROR); + } else { + // @ts-expect-error: this is ok + return ref.current[name](...args); + } + }; + return acc; + }, {}), + isReady: () => { + if (ref.current == null) { + return false; + } + + return ref.current.isReady(); + }, + current: null, + }; + + return ref; +} diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index 99dfbc7c..c8938c37 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -3,6 +3,9 @@ export * from '@react-navigation/routers'; export { default as BaseNavigationContainer } from './BaseNavigationContainer'; export { default as createNavigatorFactory } from './createNavigatorFactory'; +export { default as createNavigationContainerRef } from './createNavigationContainerRef'; +export { default as useNavigationContainerRef } from './useNavigationContainerRef'; + export { default as NavigationHelpersContext } from './NavigationHelpersContext'; export { default as NavigationContext } from './NavigationContext'; export { default as NavigationRouteContext } from './NavigationRouteContext'; diff --git a/packages/core/src/types.tsx b/packages/core/src/types.tsx index bab52d4e..2ce05bac 100644 --- a/packages/core/src/types.tsx +++ b/packages/core/src/types.tsx @@ -485,7 +485,9 @@ export type NavigationContainerEventMap = { }; }; -export type NavigationContainerRef = NavigationHelpers & +export type NavigationContainerRef< + ParamList extends ParamListBase +> = NavigationHelpers & EventConsumer & { /** * Reset the navigation state of the root navigator to the provided state. @@ -505,8 +507,18 @@ export type NavigationContainerRef = NavigationHelpers & * Get the currently focused route's options. */ getCurrentOptions(): object | undefined; + /** + * Whether the navigation container is ready to handle actions. + */ + isReady(): boolean; }; +export type NavigationContainerRefWithCurrent< + ParamList extends ParamListBase +> = NavigationContainerRef & { + current: NavigationContainerRef | null; +}; + export type TypedNavigator< ParamList extends ParamListBase, State extends NavigationState, diff --git a/packages/core/src/useNavigationContainerRef.tsx b/packages/core/src/useNavigationContainerRef.tsx new file mode 100644 index 00000000..be50ef8b --- /dev/null +++ b/packages/core/src/useNavigationContainerRef.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import type { ParamListBase } from '@react-navigation/routers'; +import createNavigationContainerRef from './createNavigationContainerRef'; +import type { NavigationContainerRefWithCurrent } from './types'; + +export default function useNavigationContainerRef< + ParamList extends ParamListBase +>(): NavigationContainerRefWithCurrent { + const navigation = React.useRef | null>( + null + ); + + if (navigation.current == null) { + navigation.current = createNavigationContainerRef(); + } + + return navigation.current; +} diff --git a/packages/devtools/src/useReduxDevToolsExtension.tsx b/packages/devtools/src/useReduxDevToolsExtension.tsx index 73d1b1e9..089781e1 100644 --- a/packages/devtools/src/useReduxDevToolsExtension.tsx +++ b/packages/devtools/src/useReduxDevToolsExtension.tsx @@ -3,6 +3,7 @@ import type { NavigationContainerRef, NavigationState, NavigationAction, + ParamListBase, } from '@react-navigation/core'; import deepEqual from 'deep-equal'; @@ -22,7 +23,7 @@ type DevToolsExtension = { declare const __REDUX_DEVTOOLS_EXTENSION__: DevToolsExtension | undefined; export default function useReduxDevToolsExtension( - ref: React.RefObject + ref: React.RefObject> ) { const devToolsRef = React.useRef(); diff --git a/packages/native/src/NavigationContainer.tsx b/packages/native/src/NavigationContainer.tsx index 9e75f902..bb63259b 100644 --- a/packages/native/src/NavigationContainer.tsx +++ b/packages/native/src/NavigationContainer.tsx @@ -3,6 +3,7 @@ import { BaseNavigationContainer, NavigationContainerProps, NavigationContainerRef, + ParamListBase, } from '@react-navigation/core'; import ThemeProvider from './theming/ThemeProvider'; import DefaultTheme from './theming/DefaultTheme'; @@ -44,11 +45,13 @@ const NavigationContainer = React.forwardRef(function NavigationContainer( onReady, ...rest }: Props, - ref?: React.Ref + ref?: React.Ref | null> ) { const isLinkingEnabled = linking ? linking.enabled !== false : false; - const refContainer = React.useRef(null); + const refContainer = React.useRef>( + null + ); useBackButton(refContainer); useDocumentTitle(refContainer, documentTitle); diff --git a/packages/native/src/__tests__/NavigationContainer.test.tsx b/packages/native/src/__tests__/NavigationContainer.test.tsx index 477dd9c1..b8721d4f 100644 --- a/packages/native/src/__tests__/NavigationContainer.test.tsx +++ b/packages/native/src/__tests__/NavigationContainer.test.tsx @@ -5,7 +5,8 @@ import { StackRouter, TabRouter, NavigationHelpersContext, - NavigationContainerRef, + createNavigationContainerRef, + ParamListBase, } from '@react-navigation/core'; import { act, render } from '@testing-library/react-native'; import NavigationContainer from '../NavigationContainer'; @@ -79,7 +80,7 @@ it('integrates with the history API', () => { }, }; - const navigation = React.createRef(); + const navigation = createNavigationContainerRef(); render( diff --git a/packages/native/src/__tests__/useLinking.test.tsx b/packages/native/src/__tests__/useLinking.test.tsx index 85052629..ced80a87 100644 --- a/packages/native/src/__tests__/useLinking.test.tsx +++ b/packages/native/src/__tests__/useLinking.test.tsx @@ -1,10 +1,13 @@ import * as React from 'react'; import { render, RenderAPI } from '@testing-library/react-native'; -import type { NavigationContainerRef } from '@react-navigation/core'; +import { + createNavigationContainerRef, + ParamListBase, +} from '@react-navigation/core'; import useLinking from '../useLinking'; it('throws if multiple instances of useLinking are used', () => { - const ref = React.createRef(); + const ref = createNavigationContainerRef(); const options = { prefixes: [] }; diff --git a/packages/native/src/useBackButton.tsx b/packages/native/src/useBackButton.tsx index b81b8aed..9b9bb984 100644 --- a/packages/native/src/useBackButton.tsx +++ b/packages/native/src/useBackButton.tsx @@ -1,9 +1,12 @@ import * as React from 'react'; import { BackHandler } from 'react-native'; -import type { NavigationContainerRef } from '@react-navigation/core'; +import type { + NavigationContainerRef, + ParamListBase, +} from '@react-navigation/core'; export default function useBackButton( - ref: React.RefObject + ref: React.RefObject> ) { React.useEffect(() => { const subscription = BackHandler.addEventListener( diff --git a/packages/native/src/useDocumentTitle.tsx b/packages/native/src/useDocumentTitle.tsx index 430ef24a..5e49511f 100644 --- a/packages/native/src/useDocumentTitle.tsx +++ b/packages/native/src/useDocumentTitle.tsx @@ -1,12 +1,15 @@ import * as React from 'react'; -import type { NavigationContainerRef } from '@react-navigation/core'; +import type { + NavigationContainerRef, + ParamListBase, +} from '@react-navigation/core'; import type { DocumentTitleOptions } from './types'; /** * Set the document title for the active screen */ export default function useDocumentTitle( - ref: React.RefObject, + ref: React.RefObject>, { enabled = true, formatter = (options, route) => options?.title ?? route?.name, diff --git a/packages/native/src/useLinking.native.tsx b/packages/native/src/useLinking.native.tsx index 88541b3e..05c7638e 100644 --- a/packages/native/src/useLinking.native.tsx +++ b/packages/native/src/useLinking.native.tsx @@ -4,6 +4,7 @@ import { getActionFromState, getStateFromPath as getStateFromPathDefault, NavigationContainerRef, + ParamListBase, } from '@react-navigation/core'; import extractPathFromURL from './extractPathFromURL'; import type { LinkingOptions } from './types'; @@ -13,7 +14,7 @@ type ResultState = ReturnType; let isUsingLinking = false; export default function useLinking( - ref: React.RefObject, + ref: React.RefObject>, { enabled = true, prefixes, diff --git a/packages/native/src/useLinking.tsx b/packages/native/src/useLinking.tsx index 66f373ab..daa9ba6f 100644 --- a/packages/native/src/useLinking.tsx +++ b/packages/native/src/useLinking.tsx @@ -6,6 +6,7 @@ import { NavigationState, getActionFromState, findFocusedRoute, + ParamListBase, } from '@react-navigation/core'; import { nanoid } from 'nanoid/non-secure'; import ServerContext from './ServerContext'; @@ -288,7 +289,7 @@ const series = (cb: () => Promise) => { let isUsingLinking = false; export default function useLinking( - ref: React.RefObject, + ref: React.RefObject>, { enabled = true, config,