From 1d40279db18ab2aed12517ed3ca6af6d509477d2 Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Wed, 26 May 2021 21:06:16 +0200 Subject: [PATCH] feat: expose container ref in useNavigation --- packages/core/src/BaseNavigationContainer.tsx | 104 +++++++++++------- .../src/NavigationContainerRefContext.tsx | 12 ++ packages/core/src/NavigationRouteContext.tsx | 4 +- .../core/src/__tests__/useNavigation.test.tsx | 29 ++++- packages/core/src/index.tsx | 1 + packages/core/src/useNavigation.tsx | 8 +- packages/native/src/useLinkProps.tsx | 8 +- packages/native/src/useLinkTo.tsx | 18 +-- 8 files changed, 124 insertions(+), 60 deletions(-) create mode 100644 packages/core/src/NavigationContainerRefContext.tsx diff --git a/packages/core/src/BaseNavigationContainer.tsx b/packages/core/src/BaseNavigationContainer.tsx index a2356d1d..b0c19728 100644 --- a/packages/core/src/BaseNavigationContainer.tsx +++ b/packages/core/src/BaseNavigationContainer.tsx @@ -29,6 +29,7 @@ import type { NavigationContainerRef, NavigationContainerProps, } from './types'; +import NavigationContainerRefContext from './NavigationContainerRefContext'; type State = NavigationState | PartialState | undefined; @@ -136,17 +137,22 @@ const BaseNavigationContainer = React.forwardRef( const { keyedListeners, addKeyedListener } = useKeyedChildListeners(); - const dispatch = ( - action: NavigationAction | ((state: NavigationState) => NavigationAction) - ) => { - if (listeners.focus[0] == null) { - console.error(NOT_INITIALIZED_ERROR); - } else { - listeners.focus[0]((navigation) => navigation.dispatch(action)); - } - }; + const dispatch = React.useCallback( + ( + action: + | NavigationAction + | ((state: NavigationState) => NavigationAction) + ) => { + if (listeners.focus[0] == null) { + console.error(NOT_INITIALIZED_ERROR); + } else { + listeners.focus[0]((navigation) => navigation.dispatch(action)); + } + }, + [listeners.focus] + ); - const canGoBack = () => { + const canGoBack = React.useCallback(() => { if (listeners.focus[0] == null) { return false; } @@ -160,7 +166,7 @@ const BaseNavigationContainer = React.forwardRef( } else { return false; } - }; + }, [listeners.focus]); const resetRoot = React.useCallback( (state?: PartialState | NavigationState) => { @@ -200,24 +206,38 @@ const BaseNavigationContainer = React.forwardRef( const { addOptionsGetter, getCurrentOptions } = useOptionsGetters({}); - React.useImperativeHandle(ref, () => ({ - ...Object.keys(CommonActions).reduce((acc, name) => { - acc[name] = (...args: any[]) => - // @ts-expect-error: this is ok - dispatch(CommonActions[name](...args)); - return acc; - }, {}), - ...emitter.create('root'), - resetRoot, - dispatch, - canGoBack, - getRootState, - getState: () => state, - getParent: () => undefined, - getCurrentRoute, - getCurrentOptions, - isReady: () => listeners.focus[0] != null, - })); + const navigation: NavigationContainerRef = React.useMemo( + () => ({ + ...Object.keys(CommonActions).reduce((acc, name) => { + acc[name] = (...args: any[]) => + // @ts-expect-error: this is ok + dispatch(CommonActions[name](...args)); + return acc; + }, {}), + ...emitter.create('root'), + resetRoot, + dispatch, + canGoBack, + getRootState, + getState: () => stateRef.current, + getParent: () => undefined, + getCurrentRoute, + getCurrentOptions, + isReady: () => listeners.focus[0] != null, + }), + [ + canGoBack, + dispatch, + emitter, + getCurrentOptions, + getCurrentRoute, + getRootState, + listeners.focus, + resetRoot, + ] + ); + + React.useImperativeHandle(ref, () => navigation, [navigation]); const onDispatchAction = React.useCallback( (action: NavigationAction, noop: boolean) => { @@ -285,10 +305,12 @@ const BaseNavigationContainer = React.forwardRef( ); const onStateChangeRef = React.useRef(onStateChange); + const stateRef = React.useRef(state); React.useEffect(() => { isInitialRef.current = false; onStateChangeRef.current = onStateChange; + stateRef.current = state; }); React.useEffect(() => { @@ -415,17 +437,19 @@ const BaseNavigationContainer = React.forwardRef( ); let element = ( - - - - - {children} - - - - + + + + + + {children} + + + + + ); if (independent) { diff --git a/packages/core/src/NavigationContainerRefContext.tsx b/packages/core/src/NavigationContainerRefContext.tsx new file mode 100644 index 00000000..a5f27f5e --- /dev/null +++ b/packages/core/src/NavigationContainerRefContext.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; +import type { ParamListBase } from '@react-navigation/routers'; +import type { NavigationContainerRef } from './types'; + +/** + * Context which holds the route prop for a screen. + */ +const NavigationContainerRefContext = React.createContext< + NavigationContainerRef | undefined +>(undefined); + +export default NavigationContainerRefContext; diff --git a/packages/core/src/NavigationRouteContext.tsx b/packages/core/src/NavigationRouteContext.tsx index da5e16dc..cd7ee623 100644 --- a/packages/core/src/NavigationRouteContext.tsx +++ b/packages/core/src/NavigationRouteContext.tsx @@ -4,8 +4,8 @@ import type { Route } from '@react-navigation/routers'; /** * Context which holds the route prop for a screen. */ -const NavigationContext = React.createContext | undefined>( +const NavigationRouteContext = React.createContext | undefined>( undefined ); -export default NavigationContext; +export default NavigationRouteContext; diff --git a/packages/core/src/__tests__/useNavigation.test.tsx b/packages/core/src/__tests__/useNavigation.test.tsx index 9639e884..8da863ea 100644 --- a/packages/core/src/__tests__/useNavigation.test.tsx +++ b/packages/core/src/__tests__/useNavigation.test.tsx @@ -106,13 +106,40 @@ it("gets navigation's parent's parent from context", () => { ); }); +it('gets navigation from container from context', () => { + expect.assertions(1); + + const TestNavigator = (props: any): any => { + const { state, descriptors } = useNavigationBuilder(MockRouter, props); + + return state.routes.map((route) => descriptors[route.key].render()); + }; + + const Test = () => { + const navigation = useNavigation(); + + expect(navigation.navigate).toBeDefined(); + + return null; + }; + + render( + + + + {() => null} + + + ); +}); + it('throws if called outside a navigation context', () => { expect.assertions(1); const Test = () => { // eslint-disable-next-line react-hooks/rules-of-hooks expect(() => useNavigation()).toThrow( - "Couldn't find a navigation object. Is your component inside a screen in a navigator?" + "Couldn't find a navigation object. Is your component inside NavigationContainer?" ); return null; diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index c8938c37..271eb22b 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -6,6 +6,7 @@ export { default as createNavigatorFactory } from './createNavigatorFactory'; export { default as createNavigationContainerRef } from './createNavigationContainerRef'; export { default as useNavigationContainerRef } from './useNavigationContainerRef'; +export { default as NavigationContainerRefContext } from './NavigationContainerRefContext'; export { default as NavigationHelpersContext } from './NavigationHelpersContext'; export { default as NavigationContext } from './NavigationContext'; export { default as NavigationRouteContext } from './NavigationRouteContext'; diff --git a/packages/core/src/useNavigation.tsx b/packages/core/src/useNavigation.tsx index ba0bc97e..cbdd3102 100644 --- a/packages/core/src/useNavigation.tsx +++ b/packages/core/src/useNavigation.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import NavigationContainerRefContext from './NavigationContainerRefContext'; import NavigationContext from './NavigationContext'; import type { NavigationProp } from './types'; @@ -10,14 +11,15 @@ import type { NavigationProp } from './types'; export default function useNavigation< T = NavigationProp >(): T { + const root = React.useContext(NavigationContainerRefContext); const navigation = React.useContext(NavigationContext); - if (navigation === undefined) { + if (navigation === undefined && root === undefined) { throw new Error( - "Couldn't find a navigation object. Is your component inside a screen in a navigator?" + "Couldn't find a navigation object. Is your component inside NavigationContainer?" ); } // FIXME: Figure out a better way to do this - return (navigation as unknown) as T; + return ((navigation ?? root) as unknown) as T; } diff --git a/packages/native/src/useLinkProps.tsx b/packages/native/src/useLinkProps.tsx index fa01480f..5fe74a2f 100644 --- a/packages/native/src/useLinkProps.tsx +++ b/packages/native/src/useLinkProps.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { Platform, GestureResponderEvent } from 'react-native'; import { NavigationAction, + NavigationContainerRefContext, NavigationHelpersContext, } from '@react-navigation/core'; import useLinkTo, { To } from './useLinkTo'; @@ -20,6 +21,7 @@ type Props = { export default function useLinkProps< ParamList extends ReactNavigation.RootParamList >({ to, action }: Props) { + const root = React.useContext(NavigationContainerRefContext); const navigation = React.useContext(NavigationHelpersContext); const linkTo = useLinkTo(); @@ -47,8 +49,12 @@ export default function useLinkProps< if (action) { if (navigation) { navigation.dispatch(action); + } else if (root) { + root.dispatch(action); } else { - throw new Error("Couldn't find a navigation object."); + throw new Error( + "Couldn't find a navigation object. Is your component inside NavigationContainer?" + ); } } else { linkTo(to); diff --git a/packages/native/src/useLinkTo.tsx b/packages/native/src/useLinkTo.tsx index 3f8aca73..1ae4f7d2 100644 --- a/packages/native/src/useLinkTo.tsx +++ b/packages/native/src/useLinkTo.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import { + NavigationContainerRefContext, getStateFromPath, getActionFromState, - NavigationContext, } from '@react-navigation/core'; import LinkingContext from './LinkingContext'; @@ -24,25 +24,17 @@ export type To< export default function useLinkTo< ParamList extends ReactNavigation.RootParamList >() { - const navigation = React.useContext(NavigationContext); + const navigation = React.useContext(NavigationContainerRefContext); const linking = React.useContext(LinkingContext); const linkTo = React.useCallback( (to: To) => { if (navigation === undefined) { throw new Error( - "Couldn't find a navigation object. Is your component inside a screen in a navigator?" + "Couldn't find a navigation object. Is your component inside NavigationContainer?" ); } - let root = navigation; - let current; - - // Traverse up to get the root navigation - while ((current = root.getParent())) { - root = current; - } - if (typeof to !== 'string') { // @ts-expect-error: This is fine root.navigate(to.screen, to.params); @@ -63,9 +55,9 @@ export default function useLinkTo< const action = getActionFromState(state, options?.config); if (action !== undefined) { - root.dispatch(action); + navigation.dispatch(action); } else { - root.reset(state); + navigation.reset(state); } } else { throw new Error('Failed to parse the path to a navigation state.');