mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-12 22:51:18 +08:00
454 lines
14 KiB
TypeScript
454 lines
14 KiB
TypeScript
import {
|
|
CommonActions,
|
|
InitialState,
|
|
NavigationAction,
|
|
NavigationState,
|
|
ParamListBase,
|
|
PartialState,
|
|
Route,
|
|
} from '@react-navigation/routers';
|
|
import * as React from 'react';
|
|
|
|
import checkDuplicateRouteNames from './checkDuplicateRouteNames';
|
|
import checkSerializable from './checkSerializable';
|
|
import { NOT_INITIALIZED_ERROR } from './createNavigationContainerRef';
|
|
import EnsureSingleNavigator from './EnsureSingleNavigator';
|
|
import findFocusedRoute from './findFocusedRoute';
|
|
import NavigationBuilderContext from './NavigationBuilderContext';
|
|
import NavigationContainerRefContext from './NavigationContainerRefContext';
|
|
import NavigationContext from './NavigationContext';
|
|
import NavigationRouteContext from './NavigationRouteContext';
|
|
import NavigationStateContext from './NavigationStateContext';
|
|
import type {
|
|
NavigationContainerEventMap,
|
|
NavigationContainerProps,
|
|
NavigationContainerRef,
|
|
} from './types';
|
|
import UnhandledActionContext from './UnhandledActionContext';
|
|
import useChildListeners from './useChildListeners';
|
|
import useEventEmitter from './useEventEmitter';
|
|
import useKeyedChildListeners from './useKeyedChildListeners';
|
|
import useOptionsGetters from './useOptionsGetters';
|
|
import { ScheduleUpdateContext } from './useScheduleUpdate';
|
|
import useSyncState from './useSyncState';
|
|
|
|
type State = NavigationState | PartialState<NavigationState> | undefined;
|
|
|
|
const serializableWarnings: string[] = [];
|
|
const duplicateNameWarnings: string[] = [];
|
|
|
|
/**
|
|
* Remove `key` and `routeNames` from the state objects recursively to get partial state.
|
|
*
|
|
* @param state Initial state object.
|
|
*/
|
|
const getPartialState = (
|
|
state: InitialState | undefined
|
|
): PartialState<NavigationState> | undefined => {
|
|
if (state === undefined) {
|
|
return;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const { key, routeNames, ...partialState } = state;
|
|
|
|
return {
|
|
...partialState,
|
|
stale: true,
|
|
routes: state.routes.map((route) => {
|
|
if (route.state === undefined) {
|
|
return route as Route<string> & {
|
|
state?: PartialState<NavigationState>;
|
|
};
|
|
}
|
|
|
|
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,
|
|
onUnhandledAction,
|
|
independent,
|
|
children,
|
|
}: NavigationContainerProps,
|
|
ref?: React.Ref<NavigationContainerRef<ParamListBase>>
|
|
) {
|
|
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}' explicitly. Note that this will make the child navigators disconnected from the parent and you won't be able to navigate between them."
|
|
);
|
|
}
|
|
|
|
const [state, getState, setState, scheduleUpdate, flushUpdates] =
|
|
useSyncState<State>(() =>
|
|
getPartialState(initialState == null ? undefined : initialState)
|
|
);
|
|
|
|
const isFirstMountRef = React.useRef<boolean>(true);
|
|
|
|
const navigatorKeyRef = React.useRef<string | undefined>();
|
|
|
|
const getKey = React.useCallback(() => navigatorKeyRef.current, []);
|
|
|
|
const setKey = React.useCallback((key: string) => {
|
|
navigatorKeyRef.current = key;
|
|
}, []);
|
|
|
|
const { listeners, addListener } = useChildListeners();
|
|
|
|
const { keyedListeners, addKeyedListener } = useKeyedChildListeners();
|
|
|
|
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 = React.useCallback(() => {
|
|
if (listeners.focus[0] == null) {
|
|
return false;
|
|
}
|
|
|
|
const { result, handled } = listeners.focus[0]((navigation) =>
|
|
navigation.canGoBack()
|
|
);
|
|
|
|
if (handled) {
|
|
return result;
|
|
} else {
|
|
return false;
|
|
}
|
|
}, [listeners.focus]);
|
|
|
|
const resetRoot = React.useCallback(
|
|
(state?: PartialState<NavigationState> | NavigationState) => {
|
|
const target = state?.key ?? keyedListeners.getState.root?.().key;
|
|
|
|
if (target == null) {
|
|
console.error(NOT_INITIALIZED_ERROR);
|
|
} else {
|
|
listeners.focus[0]((navigation) =>
|
|
navigation.dispatch({
|
|
...CommonActions.reset(state),
|
|
target,
|
|
})
|
|
);
|
|
}
|
|
},
|
|
[keyedListeners.getState, listeners.focus]
|
|
);
|
|
|
|
const getRootState = React.useCallback(() => {
|
|
return keyedListeners.getState.root?.();
|
|
}, [keyedListeners.getState]);
|
|
|
|
const getCurrentRoute = React.useCallback(() => {
|
|
const state = getRootState();
|
|
|
|
if (state == null) {
|
|
return undefined;
|
|
}
|
|
|
|
const route = findFocusedRoute(state);
|
|
|
|
return route as Route<string> | undefined;
|
|
}, [getRootState]);
|
|
|
|
const emitter = useEventEmitter<NavigationContainerEventMap>();
|
|
|
|
const { addOptionsGetter, getCurrentOptions } = useOptionsGetters({});
|
|
|
|
const navigation: NavigationContainerRef<ParamListBase> = React.useMemo(
|
|
() => ({
|
|
...Object.keys(CommonActions).reduce<any>((acc, name) => {
|
|
acc[name] = (...args: any[]) =>
|
|
// @ts-expect-error: this is ok
|
|
dispatch(CommonActions[name](...args));
|
|
return acc;
|
|
}, {}),
|
|
...emitter.create('root'),
|
|
dispatch,
|
|
resetRoot,
|
|
isFocused: () => true,
|
|
canGoBack,
|
|
getParent: () => undefined,
|
|
getState: () => stateRef.current,
|
|
getRootState,
|
|
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) => {
|
|
emitter.emit({
|
|
type: '__unsafe_action__',
|
|
data: { action, noop, stack: stackRef.current },
|
|
});
|
|
},
|
|
[emitter]
|
|
);
|
|
|
|
const lastEmittedOptionsRef = React.useRef<object | undefined>();
|
|
|
|
const onOptionsChange = React.useCallback(
|
|
(options) => {
|
|
if (lastEmittedOptionsRef.current === options) {
|
|
return;
|
|
}
|
|
|
|
lastEmittedOptionsRef.current = options;
|
|
|
|
emitter.emit({
|
|
type: 'options',
|
|
data: { options },
|
|
});
|
|
},
|
|
[emitter]
|
|
);
|
|
|
|
const stackRef = React.useRef<string | undefined>();
|
|
|
|
const builderContext = React.useMemo(
|
|
() => ({
|
|
addListener,
|
|
addKeyedListener,
|
|
onDispatchAction,
|
|
onOptionsChange,
|
|
stackRef,
|
|
}),
|
|
[addListener, addKeyedListener, onDispatchAction, onOptionsChange]
|
|
);
|
|
|
|
const scheduleContext = React.useMemo(
|
|
() => ({ scheduleUpdate, flushUpdates }),
|
|
[scheduleUpdate, flushUpdates]
|
|
);
|
|
|
|
const isInitialRef = React.useRef(true);
|
|
|
|
const getIsInitial = React.useCallback(() => isInitialRef.current, []);
|
|
|
|
const context = React.useMemo(
|
|
() => ({
|
|
state,
|
|
getState,
|
|
setState,
|
|
getKey,
|
|
setKey,
|
|
getIsInitial,
|
|
addOptionsGetter,
|
|
}),
|
|
[
|
|
state,
|
|
getState,
|
|
setState,
|
|
getKey,
|
|
setKey,
|
|
getIsInitial,
|
|
addOptionsGetter,
|
|
]
|
|
);
|
|
|
|
const onStateChangeRef = React.useRef(onStateChange);
|
|
const stateRef = React.useRef(state);
|
|
|
|
React.useEffect(() => {
|
|
isInitialRef.current = false;
|
|
onStateChangeRef.current = onStateChange;
|
|
stateRef.current = state;
|
|
});
|
|
|
|
React.useEffect(() => {
|
|
const hydratedState = getRootState();
|
|
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
if (hydratedState !== undefined) {
|
|
const serializableResult = checkSerializable(hydratedState);
|
|
|
|
if (!serializableResult.serializable) {
|
|
const { location, reason } = serializableResult;
|
|
|
|
let path = '';
|
|
let pointer: Record<any, any> = hydratedState;
|
|
let params = false;
|
|
|
|
for (let i = 0; i < location.length; i++) {
|
|
const curr = location[i];
|
|
const prev = location[i - 1];
|
|
|
|
pointer = pointer[curr];
|
|
|
|
if (!params && curr === 'state') {
|
|
continue;
|
|
} else if (!params && curr === 'routes') {
|
|
if (path) {
|
|
path += ' > ';
|
|
}
|
|
} else if (
|
|
!params &&
|
|
typeof curr === 'number' &&
|
|
prev === 'routes'
|
|
) {
|
|
path += pointer?.name;
|
|
} else if (!params) {
|
|
path += ` > ${curr}`;
|
|
params = true;
|
|
} else {
|
|
if (typeof curr === 'number' || /^[0-9]+$/.test(curr)) {
|
|
path += `[${curr}]`;
|
|
} else if (/^[a-z$_]+$/i.test(curr)) {
|
|
path += `.${curr}`;
|
|
} else {
|
|
path += `[${JSON.stringify(curr)}]`;
|
|
}
|
|
}
|
|
}
|
|
|
|
const message = `Non-serializable values were found in the navigation state. Check:\n\n${path} (${reason})\n\nThis 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 components with callbacks in your options, you can use 'navigation.setOptions' instead. See https://reactnavigation.org/docs/troubleshooting#i-get-the-warning-non-serializable-values-were-found-in-the-navigation-state for more details.`;
|
|
|
|
if (!serializableWarnings.includes(message)) {
|
|
serializableWarnings.push(message);
|
|
console.warn(message);
|
|
}
|
|
}
|
|
|
|
const duplicateRouteNamesResult =
|
|
checkDuplicateRouteNames(hydratedState);
|
|
|
|
if (duplicateRouteNamesResult.length) {
|
|
const message = `Found screens with the same name nested inside one another. Check:\n${duplicateRouteNamesResult.map(
|
|
(locations) => `\n${locations.join(', ')}`
|
|
)}\n\nThis can cause confusing behavior during navigation. Consider using unique names for each screen instead.`;
|
|
|
|
if (!duplicateNameWarnings.includes(message)) {
|
|
duplicateNameWarnings.push(message);
|
|
console.warn(message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
emitter.emit({ type: 'state', data: { state } });
|
|
|
|
if (!isFirstMountRef.current && onStateChangeRef.current) {
|
|
onStateChangeRef.current(hydratedState);
|
|
}
|
|
|
|
isFirstMountRef.current = false;
|
|
}, [getRootState, emitter, state]);
|
|
|
|
const defaultOnUnhandledAction = React.useCallback(
|
|
(action: NavigationAction) => {
|
|
if (process.env.NODE_ENV === 'production') {
|
|
return;
|
|
}
|
|
|
|
const payload: Record<string, any> | undefined = action.payload;
|
|
|
|
let message = `The action '${action.type}'${
|
|
payload ? ` with payload ${JSON.stringify(action.payload)}` : ''
|
|
} was not handled by any navigator.`;
|
|
|
|
switch (action.type) {
|
|
case 'NAVIGATE':
|
|
case 'PUSH':
|
|
case 'REPLACE':
|
|
case 'JUMP_TO':
|
|
if (payload?.name) {
|
|
message += `\n\nDo you have a screen named '${payload.name}'?\n\nIf you're trying to navigate to a screen in a nested navigator, see https://reactnavigation.org/docs/nesting-navigators#navigating-to-a-screen-in-a-nested-navigator.`;
|
|
} else {
|
|
message += `\n\nYou need to pass the name of the screen to navigate to.\n\nSee https://reactnavigation.org/docs/navigation-actions for usage.`;
|
|
}
|
|
|
|
break;
|
|
case 'GO_BACK':
|
|
case 'POP':
|
|
case 'POP_TO_TOP':
|
|
message += `\n\nIs there any screen to go back to?`;
|
|
break;
|
|
case 'OPEN_DRAWER':
|
|
case 'CLOSE_DRAWER':
|
|
case 'TOGGLE_DRAWER':
|
|
message += `\n\nIs your screen inside a Drawer navigator?`;
|
|
break;
|
|
}
|
|
|
|
message += `\n\nThis is a development-only warning and won't be shown in production.`;
|
|
|
|
console.error(message);
|
|
},
|
|
[]
|
|
);
|
|
|
|
let element = (
|
|
<NavigationContainerRefContext.Provider value={navigation}>
|
|
<ScheduleUpdateContext.Provider value={scheduleContext}>
|
|
<NavigationBuilderContext.Provider value={builderContext}>
|
|
<NavigationStateContext.Provider value={context}>
|
|
<UnhandledActionContext.Provider
|
|
value={onUnhandledAction ?? defaultOnUnhandledAction}
|
|
>
|
|
<EnsureSingleNavigator>{children}</EnsureSingleNavigator>
|
|
</UnhandledActionContext.Provider>
|
|
</NavigationStateContext.Provider>
|
|
</NavigationBuilderContext.Provider>
|
|
</ScheduleUpdateContext.Provider>
|
|
</NavigationContainerRefContext.Provider>
|
|
);
|
|
|
|
if (independent) {
|
|
// We need to clear any existing contexts for nested independent container to work correctly
|
|
element = (
|
|
<NavigationRouteContext.Provider value={undefined}>
|
|
<NavigationContext.Provider value={undefined}>
|
|
{element}
|
|
</NavigationContext.Provider>
|
|
</NavigationRouteContext.Provider>
|
|
);
|
|
}
|
|
|
|
return element;
|
|
}
|
|
);
|
|
|
|
export default BaseNavigationContainer;
|