feat: expose container ref in useNavigation

This commit is contained in:
Satyajit Sahoo
2021-05-26 21:06:16 +02:00
parent cde44a5785
commit 1d40279db1
8 changed files with 124 additions and 60 deletions

View File

@@ -29,6 +29,7 @@ import type {
NavigationContainerRef, NavigationContainerRef,
NavigationContainerProps, NavigationContainerProps,
} from './types'; } from './types';
import NavigationContainerRefContext from './NavigationContainerRefContext';
type State = NavigationState | PartialState<NavigationState> | undefined; type State = NavigationState | PartialState<NavigationState> | undefined;
@@ -136,17 +137,22 @@ const BaseNavigationContainer = React.forwardRef(
const { keyedListeners, addKeyedListener } = useKeyedChildListeners(); const { keyedListeners, addKeyedListener } = useKeyedChildListeners();
const dispatch = ( const dispatch = React.useCallback(
action: NavigationAction | ((state: NavigationState) => NavigationAction) (
) => { action:
if (listeners.focus[0] == null) { | NavigationAction
console.error(NOT_INITIALIZED_ERROR); | ((state: NavigationState) => NavigationAction)
} else { ) => {
listeners.focus[0]((navigation) => navigation.dispatch(action)); 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) { if (listeners.focus[0] == null) {
return false; return false;
} }
@@ -160,7 +166,7 @@ const BaseNavigationContainer = React.forwardRef(
} else { } else {
return false; return false;
} }
}; }, [listeners.focus]);
const resetRoot = React.useCallback( const resetRoot = React.useCallback(
(state?: PartialState<NavigationState> | NavigationState) => { (state?: PartialState<NavigationState> | NavigationState) => {
@@ -200,24 +206,38 @@ const BaseNavigationContainer = React.forwardRef(
const { addOptionsGetter, getCurrentOptions } = useOptionsGetters({}); const { addOptionsGetter, getCurrentOptions } = useOptionsGetters({});
React.useImperativeHandle(ref, () => ({ const navigation: NavigationContainerRef<ParamListBase> = React.useMemo(
...Object.keys(CommonActions).reduce<any>((acc, name) => { () => ({
acc[name] = (...args: any[]) => ...Object.keys(CommonActions).reduce<any>((acc, name) => {
// @ts-expect-error: this is ok acc[name] = (...args: any[]) =>
dispatch(CommonActions[name](...args)); // @ts-expect-error: this is ok
return acc; dispatch(CommonActions[name](...args));
}, {}), return acc;
...emitter.create('root'), }, {}),
resetRoot, ...emitter.create('root'),
dispatch, resetRoot,
canGoBack, dispatch,
getRootState, canGoBack,
getState: () => state, getRootState,
getParent: () => undefined, getState: () => stateRef.current,
getCurrentRoute, getParent: () => undefined,
getCurrentOptions, getCurrentRoute,
isReady: () => listeners.focus[0] != null, getCurrentOptions,
})); isReady: () => listeners.focus[0] != null,
}),
[
canGoBack,
dispatch,
emitter,
getCurrentOptions,
getCurrentRoute,
getRootState,
listeners.focus,
resetRoot,
]
);
React.useImperativeHandle(ref, () => navigation, [navigation]);
const onDispatchAction = React.useCallback( const onDispatchAction = React.useCallback(
(action: NavigationAction, noop: boolean) => { (action: NavigationAction, noop: boolean) => {
@@ -285,10 +305,12 @@ const BaseNavigationContainer = React.forwardRef(
); );
const onStateChangeRef = React.useRef(onStateChange); const onStateChangeRef = React.useRef(onStateChange);
const stateRef = React.useRef(state);
React.useEffect(() => { React.useEffect(() => {
isInitialRef.current = false; isInitialRef.current = false;
onStateChangeRef.current = onStateChange; onStateChangeRef.current = onStateChange;
stateRef.current = state;
}); });
React.useEffect(() => { React.useEffect(() => {
@@ -415,17 +437,19 @@ const BaseNavigationContainer = React.forwardRef(
); );
let element = ( let element = (
<ScheduleUpdateContext.Provider value={scheduleContext}> <NavigationContainerRefContext.Provider value={navigation}>
<NavigationBuilderContext.Provider value={builderContext}> <ScheduleUpdateContext.Provider value={scheduleContext}>
<NavigationStateContext.Provider value={context}> <NavigationBuilderContext.Provider value={builderContext}>
<UnhandledActionContext.Provider <NavigationStateContext.Provider value={context}>
value={onUnhandledAction ?? defaultOnUnhandledAction} <UnhandledActionContext.Provider
> value={onUnhandledAction ?? defaultOnUnhandledAction}
<EnsureSingleNavigator>{children}</EnsureSingleNavigator> >
</UnhandledActionContext.Provider> <EnsureSingleNavigator>{children}</EnsureSingleNavigator>
</NavigationStateContext.Provider> </UnhandledActionContext.Provider>
</NavigationBuilderContext.Provider> </NavigationStateContext.Provider>
</ScheduleUpdateContext.Provider> </NavigationBuilderContext.Provider>
</ScheduleUpdateContext.Provider>
</NavigationContainerRefContext.Provider>
); );
if (independent) { if (independent) {

View File

@@ -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<ParamListBase> | undefined
>(undefined);
export default NavigationContainerRefContext;

View File

@@ -4,8 +4,8 @@ import type { Route } from '@react-navigation/routers';
/** /**
* Context which holds the route prop for a screen. * Context which holds the route prop for a screen.
*/ */
const NavigationContext = React.createContext<Route<string> | undefined>( const NavigationRouteContext = React.createContext<Route<string> | undefined>(
undefined undefined
); );
export default NavigationContext; export default NavigationRouteContext;

View File

@@ -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(
<BaseNavigationContainer>
<Test />
<TestNavigator>
<Screen name="foo">{() => null}</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
});
it('throws if called outside a navigation context', () => { it('throws if called outside a navigation context', () => {
expect.assertions(1); expect.assertions(1);
const Test = () => { const Test = () => {
// eslint-disable-next-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks
expect(() => useNavigation()).toThrow( 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; return null;

View File

@@ -6,6 +6,7 @@ export { default as createNavigatorFactory } from './createNavigatorFactory';
export { default as createNavigationContainerRef } from './createNavigationContainerRef'; export { default as createNavigationContainerRef } from './createNavigationContainerRef';
export { default as useNavigationContainerRef } from './useNavigationContainerRef'; export { default as useNavigationContainerRef } from './useNavigationContainerRef';
export { default as NavigationContainerRefContext } from './NavigationContainerRefContext';
export { default as NavigationHelpersContext } from './NavigationHelpersContext'; export { default as NavigationHelpersContext } from './NavigationHelpersContext';
export { default as NavigationContext } from './NavigationContext'; export { default as NavigationContext } from './NavigationContext';
export { default as NavigationRouteContext } from './NavigationRouteContext'; export { default as NavigationRouteContext } from './NavigationRouteContext';

View File

@@ -1,4 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import NavigationContainerRefContext from './NavigationContainerRefContext';
import NavigationContext from './NavigationContext'; import NavigationContext from './NavigationContext';
import type { NavigationProp } from './types'; import type { NavigationProp } from './types';
@@ -10,14 +11,15 @@ import type { NavigationProp } from './types';
export default function useNavigation< export default function useNavigation<
T = NavigationProp<ReactNavigation.RootParamList> T = NavigationProp<ReactNavigation.RootParamList>
>(): T { >(): T {
const root = React.useContext(NavigationContainerRefContext);
const navigation = React.useContext(NavigationContext); const navigation = React.useContext(NavigationContext);
if (navigation === undefined) { if (navigation === undefined && root === undefined) {
throw new Error( 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 // FIXME: Figure out a better way to do this
return (navigation as unknown) as T; return ((navigation ?? root) as unknown) as T;
} }

View File

@@ -2,6 +2,7 @@ import * as React from 'react';
import { Platform, GestureResponderEvent } from 'react-native'; import { Platform, GestureResponderEvent } from 'react-native';
import { import {
NavigationAction, NavigationAction,
NavigationContainerRefContext,
NavigationHelpersContext, NavigationHelpersContext,
} from '@react-navigation/core'; } from '@react-navigation/core';
import useLinkTo, { To } from './useLinkTo'; import useLinkTo, { To } from './useLinkTo';
@@ -20,6 +21,7 @@ type Props<ParamList extends ReactNavigation.RootParamList> = {
export default function useLinkProps< export default function useLinkProps<
ParamList extends ReactNavigation.RootParamList ParamList extends ReactNavigation.RootParamList
>({ to, action }: Props<ParamList>) { >({ to, action }: Props<ParamList>) {
const root = React.useContext(NavigationContainerRefContext);
const navigation = React.useContext(NavigationHelpersContext); const navigation = React.useContext(NavigationHelpersContext);
const linkTo = useLinkTo<ParamList>(); const linkTo = useLinkTo<ParamList>();
@@ -47,8 +49,12 @@ export default function useLinkProps<
if (action) { if (action) {
if (navigation) { if (navigation) {
navigation.dispatch(action); navigation.dispatch(action);
} else if (root) {
root.dispatch(action);
} else { } 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 { } else {
linkTo(to); linkTo(to);

View File

@@ -1,8 +1,8 @@
import * as React from 'react'; import * as React from 'react';
import { import {
NavigationContainerRefContext,
getStateFromPath, getStateFromPath,
getActionFromState, getActionFromState,
NavigationContext,
} from '@react-navigation/core'; } from '@react-navigation/core';
import LinkingContext from './LinkingContext'; import LinkingContext from './LinkingContext';
@@ -24,25 +24,17 @@ export type To<
export default function useLinkTo< export default function useLinkTo<
ParamList extends ReactNavigation.RootParamList ParamList extends ReactNavigation.RootParamList
>() { >() {
const navigation = React.useContext(NavigationContext); const navigation = React.useContext(NavigationContainerRefContext);
const linking = React.useContext(LinkingContext); const linking = React.useContext(LinkingContext);
const linkTo = React.useCallback( const linkTo = React.useCallback(
(to: To<ParamList>) => { (to: To<ParamList>) => {
if (navigation === undefined) { if (navigation === undefined) {
throw new Error( 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') { if (typeof to !== 'string') {
// @ts-expect-error: This is fine // @ts-expect-error: This is fine
root.navigate(to.screen, to.params); root.navigate(to.screen, to.params);
@@ -63,9 +55,9 @@ export default function useLinkTo<
const action = getActionFromState(state, options?.config); const action = getActionFromState(state, options?.config);
if (action !== undefined) { if (action !== undefined) {
root.dispatch(action); navigation.dispatch(action);
} else { } else {
root.reset(state); navigation.reset(state);
} }
} else { } else {
throw new Error('Failed to parse the path to a navigation state.'); throw new Error('Failed to parse the path to a navigation state.');