Files
react-navigation/packages/core/src/useNavigationCache.tsx
2020-06-24 20:07:32 +02:00

126 lines
3.7 KiB
TypeScript

import * as React from 'react';
import {
CommonActions,
NavigationAction,
ParamListBase,
NavigationState,
Router,
} from '@react-navigation/routers';
import type { NavigationEventEmitter } from './useEventEmitter';
import type { EventMapBase, NavigationHelpers, NavigationProp } from './types';
type Options<State extends NavigationState> = {
state: State;
getState: () => State;
navigation: NavigationHelpers<ParamListBase> &
Partial<NavigationProp<ParamListBase, string, any, any, any>>;
setOptions: (
cb: (options: Record<string, object>) => Record<string, object>
) => void;
router: Router<State, NavigationAction>;
emitter: NavigationEventEmitter<EventMapBase>;
};
type NavigationCache<
State extends NavigationState,
ScreenOptions extends {}
> = {
[key: string]: NavigationProp<ParamListBase, string, State, ScreenOptions>;
};
/**
* Hook to cache navigation objects for each screen in the navigator.
* It's important to cache them to make sure navigation objects don't change between renders.
* This lets us apply optimizations like `React.memo` to minimize re-rendering screens.
*/
export default function useNavigationCache<
State extends NavigationState,
ScreenOptions extends {}
>({
state,
getState,
navigation,
setOptions,
router,
emitter,
}: Options<State>) {
// Cache object which holds navigation objects for each screen
// We use `React.useMemo` instead of `React.useRef` coz we want to invalidate it when deps change
// In reality, these deps will rarely change, if ever
const cache = React.useMemo(
() => ({ current: {} as NavigationCache<State, ScreenOptions> }),
// eslint-disable-next-line react-hooks/exhaustive-deps
[getState, navigation, setOptions, router, emitter]
);
const actions = {
...router.actionCreators,
...CommonActions,
};
cache.current = state.routes.reduce<NavigationCache<State, ScreenOptions>>(
(acc, route) => {
const previous = cache.current[route.key];
if (previous) {
// If a cached navigation object already exists, reuse it
acc[route.key] = previous;
} else {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { emit, ...rest } = navigation;
const dispatch = (
action: NavigationAction | ((state: State) => NavigationAction)
) => {
const payload =
typeof action === 'function' ? action(getState()) : action;
navigation.dispatch(
typeof payload === 'object' && payload != null
? { source: route.key, ...payload }
: payload
);
};
const helpers = Object.keys(actions).reduce<Record<string, () => void>>(
(acc, name) => {
// @ts-expect-error: name is a valid key, but TypeScript is dumb
acc[name] = (...args: any) => dispatch(actions[name](...args));
return acc;
},
{}
);
acc[route.key] = {
...rest,
...helpers,
...emitter.create(route.key),
dispatch,
setOptions: (options: object) =>
setOptions((o) => ({
...o,
[route.key]: { ...o[route.key], ...options },
})),
isFocused: () => {
const state = getState();
if (state.routes[state.index].key !== route.key) {
return false;
}
// If the current screen is focused, we also need to check if parent navigator is focused
// This makes sure that we return the focus state in the whole tree, not just this navigator
return navigation ? navigation.isFocused() : true;
},
};
}
return acc;
},
{}
);
return cache.current;
}