Skip to content

Commit

Permalink
feat: add a new 'describe' method to create placeholder descriptor (#…
Browse files Browse the repository at this point in the history
…11726)

Please provide enough information so that others can review your pull
request.

**Motivation**

Explain the **motivation** for making this change. What existing problem
does the pull request solve?

If this pull request addresses an existing issue, link to the issue. If
an issue is not present, describe the issue here.

**Test plan**

Describe the **steps to test this change** so that a reviewer can verify
it. Provide screenshots or videos if the change affects UI.

The change must pass lint, typescript and tests.

---------

Co-authored-by: Satyajit Sahoo <satyajit.happy@gmail.com>
  • Loading branch information
osdnk and satya164 committed Dec 7, 2023
1 parent 382d6e6 commit 0b83a9b
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 48 deletions.
2 changes: 1 addition & 1 deletion packages/core/src/__tests__/useNavigationCache.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ it('preserves reference for navigation objects', () => {
const previous = React.useRef<any>();

const emitter = useEventEmitter();
const navigations = useNavigationCache({
const { navigations } = useNavigationCache({
state,
getState,
navigation,
Expand Down
124 changes: 101 additions & 23 deletions packages/core/src/useDescriptors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
NavigationAction,
NavigationState,
ParamListBase,
PartialState,
Router,
} from '@react-navigation/routers';
import * as React from 'react';
Expand Down Expand Up @@ -125,7 +126,11 @@ export function useDescriptors<
]
);

const navigations = useNavigationCache<State, ScreenOptions, EventMap>({
const { base, navigations } = useNavigationCache<
State,
ScreenOptions,
EventMap
>({
state,
getState,
navigation,
Expand All @@ -136,27 +141,20 @@ export function useDescriptors<

const routes = useRouteCache(state.routes);

return routes.reduce<
Record<
const getOptions = (
route: RouteProp<ParamListBase, string>,
navigation: NavigationProp<
ParamListBase,
string,
Descriptor<
ScreenOptions,
NavigationProp<
ParamListBase,
string,
string | undefined,
State,
ScreenOptions,
EventMap
> &
ActionHelpers,
RouteProp<ParamListBase>
>
>
>((acc, route, i) => {
string | undefined,
State,
ScreenOptions,
EventMap
>,
overrides: Record<string, ScreenOptions>
) => {
const config = screens[route.name];
const screen = config.props;
const navigation = navigations[route.key];

const optionsList = [
// The default `screenOptions` passed to the navigator
Expand All @@ -168,10 +166,10 @@ export function useDescriptors<
// The `options` prop passed to `Screen` elements,
screen.options,
// The options set via `navigation.setOptions`
options[route.key],
overrides,
];

const customOptions = optionsList.reduce<ScreenOptions>(
return optionsList.reduce<ScreenOptions>(
(acc, curr) =>
Object.assign(
acc,
Expand All @@ -180,6 +178,23 @@ export function useDescriptors<
),
{} as ScreenOptions
);
};

const render = (
route: RouteProp<ParamListBase, string>,
navigation: NavigationProp<
ParamListBase,
string,
string | undefined,
State,
ScreenOptions,
EventMap
>,
customOptions: ScreenOptions,
routeState: NavigationState | PartialState<NavigationState> | undefined
) => {
const config = screens[route.name];
const screen = config.props;

const clearOptions = () =>
setOptions((o) => {
Expand All @@ -192,15 +207,15 @@ export function useDescriptors<
return o;
});

const element = (
return (
<NavigationBuilderContext.Provider key={route.key} value={context}>
<NavigationContext.Provider value={navigation}>
<NavigationRouteContext.Provider value={route}>
<SceneView
navigation={navigation}
route={route}
screen={screen}
routeState={state.routes[i].state}
routeState={routeState}
getState={getState}
setState={setState}
options={customOptions}
Expand All @@ -210,6 +225,34 @@ export function useDescriptors<
</NavigationContext.Provider>
</NavigationBuilderContext.Provider>
);
};

const descriptors = routes.reduce<
Record<
string,
Descriptor<
ScreenOptions,
NavigationProp<
ParamListBase,
string,
string | undefined,
State,
ScreenOptions,
EventMap
> &
ActionHelpers,
RouteProp<ParamListBase>
>
>
>((acc, route, i) => {
const navigation = navigations[route.key];
const customOptions = getOptions(route, navigation, options[route.key]);
const element = render(
route,
navigation,
customOptions,
state.routes[i].state
);

acc[route.key] = {
route,
Expand All @@ -223,4 +266,39 @@ export function useDescriptors<

return acc;
}, {});

/**
* Create a descriptor object for a route.
*
* @param route Route object for which the descriptor should be created
* @param placeholder Whether the descriptor should be a placeholder, e.g. for a route not yet in the state
* @returns Descriptor object
*/
const describe = (route: RouteProp<ParamListBase>, placeholder: boolean) => {
if (!placeholder) {
if (!(route.key in descriptors)) {
throw new Error(`Couldn't find a route with the key ${route.key}.`);
}

return descriptors[route.key];
}

const navigation = base;
const customOptions = getOptions(route, navigation, {});
const element = render(route, navigation, customOptions, undefined);

return {
route,
navigation,
render() {
return element;
},
options: customOptions as ScreenOptions,
};
};

return {
describe,
descriptors,
};
}
3 changes: 2 additions & 1 deletion packages/core/src/useNavigationBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@ export function useNavigationBuilder<
getStateListeners: keyedListeners.getState,
});

const descriptors = useDescriptors<
const { describe, descriptors } = useDescriptors<
State,
ActionHelpers,
ScreenOptions,
Expand Down Expand Up @@ -727,6 +727,7 @@ export function useNavigationBuilder<
return {
state,
navigation,
describe,
descriptors,
NavigationContent,
};
Expand Down
106 changes: 83 additions & 23 deletions packages/core/src/useNavigationCache.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,25 @@ type Options<
emitter: NavigationEventEmitter<EventMap>;
};

type NavigationCache<
type NavigationItem<
State extends NavigationState,
ScreenOptions extends {},
EventMap extends Record<string, any>,
> = Record<
> = NavigationProp<
ParamListBase,
string,
NavigationProp<
ParamListBase,
string,
string | undefined,
State,
ScreenOptions,
EventMap
>
string | undefined,
State,
ScreenOptions,
EventMap
>;

type NavigationCache<
State extends NavigationState,
ScreenOptions extends {},
EventMap extends Record<string, any>,
> = Record<string, NavigationItem<State, ScreenOptions, EventMap>>;

/**
* 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.
Expand All @@ -64,20 +67,72 @@ export function useNavigationCache<
}: Options<State, ScreenOptions, EventMap>) {
const { stackRef } = React.useContext(NavigationBuilderContext);

const base = React.useMemo((): NavigationItem<
State,
ScreenOptions,
EventMap
> => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { emit, ...rest } = navigation;

const actions = {
...router.actionCreators,
...CommonActions,
};

const dispatch = () => {
throw new Error(
'Actions cannot be dispatched from a placeholder screen.'
);
};

const helpers = Object.keys(actions).reduce<Record<string, () => void>>(
(acc, name) => {
acc[name] = dispatch;

return acc;
},
{}
);

return {
...rest,
...helpers,
addListener: () => {
// Event listeners are not supported for placeholder screens

return () => {
// Empty function
};
},
removeListener: () => {
// Event listeners are not supported for placeholder screens
},
dispatch,
// @ts-expect-error: too much work to fix the types for now
getParent: (id?: string) => {
if (id !== undefined && id === rest.getId()) {
return base;
}

return rest.getParent(id);
},
setOptions: () => {
throw new Error('Options cannot be set from a placeholder screen.');
},
isFocused: () => false,
};
}, [navigation, router.actionCreators]);

// 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, EventMap> }),
// eslint-disable-next-line react-hooks/exhaustive-deps
[getState, navigation, setOptions, router, emitter]
[base, getState, navigation, setOptions, emitter]
);

const actions = {
...router.actionCreators,
...CommonActions,
};

cache.current = state.routes.reduce<
NavigationCache<State, ScreenOptions, EventMap>
>((acc, route) => {
Expand All @@ -91,9 +146,6 @@ export function useNavigationCache<
// 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 = (thunk: Thunk) => {
const action = typeof thunk === 'function' ? thunk(getState()) : thunk;

Expand Down Expand Up @@ -124,6 +176,11 @@ export function useNavigationCache<
}
};

const actions = {
...router.actionCreators,
...CommonActions,
};

const helpers = Object.keys(actions).reduce<Record<string, () => void>>(
(acc, name) => {
acc[name] = (...args: any) =>
Expand All @@ -138,19 +195,19 @@ export function useNavigationCache<
);

acc[route.key] = {
...rest,
...base,
...helpers,
// FIXME: too much work to fix the types for now
...(emitter.create(route.key) as any),
dispatch: (thunk: Thunk) => withStack(() => dispatch(thunk)),
getParent: (id?: string) => {
if (id !== undefined && id === rest.getId()) {
if (id !== undefined && id === base.getId()) {
// If the passed id is the same as the current navigation id,
// we return the cached navigation object for the relevant route
return acc[route.key];
}

return rest.getParent(id);
return base.getParent(id);
},
setOptions: (options: object) => {
setOptions((o) => ({
Expand All @@ -175,5 +232,8 @@ export function useNavigationCache<
return acc;
}, {});

return cache.current;
return {
base,
navigations: cache.current,
};
}

0 comments on commit 0b83a9b

Please sign in to comment.