mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-05-15 16:23:31 +08:00
feat: expose container ref in useNavigation
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
12
packages/core/src/NavigationContainerRefContext.tsx
Normal file
12
packages/core/src/NavigationContainerRefContext.tsx
Normal 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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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.');
|
||||||
|
|||||||
Reference in New Issue
Block a user