Skip to content

Commit

Permalink
feat: add an ID prop to navigators
Browse files Browse the repository at this point in the history
When we have nested navigators, we often may want control over which navigator should handle an action that we dispatch. This is often handled automatically, e.g. if you navigate to a screen named `Settings`, the navigator which contains the `Settings` screen will handle the action. But in some cases, it's not possible to figure out the navigator automatically.

Some example cases:

We may want to have nested drawers to have a drawer on the left, and one on the right - and still be able to open/close them. We can use `getParent` to achieve this:

```js
navigation.getParent().openDrawer();
```

However, this is prone to error if we nest another navigator inside (e.g. a stack). So we need a way to distinguish between which drawer we want to target.

An alternative way to distinguish different navigators is to specify the `key` of the navigator in the `target` field of the action:

```js
navigation.dispatch({
  ...DrawerActions.openDrawer(),
  target: 'keyOfTheDrawer',
});
```

However, this has a few problems:

- It's not straightforward to get the `key` of the navigator since the `key` is generated by React Navigation.
- It's not type-safe. Nothing ensures that the `key` is valid.

We may also want to call `setOptions` on a parent navigator. The options don't bubble up unlike the actions, so it's important make sure to call it for the correct navigator. We can use `getParent` in this case as well, but it's prone to the same set of issues as the previous case.

There are also no alternatives for this unlike specifying `key` for actions.

This commit adds a `id` prop to navigators. Example:

```js
<Drawer.Navigator id="LeftDrawer">{/* ... */}</Drawer.Navigator>
```

Then we can use it along with `getParent` as follows:

```js
navigation.getParent('LeftDrawer').openDrawer();
```

No matter how many nested navigators are added, we can always find the correct navigator this way.

To make this work with type-checking, we'd pass a third generic to our `NavigationProp` or `ScreenProps` types:

```ts
type LeftDrawerScreenProps<T extends keyof LeftDrawerParamList> =
  DrawerScreenProps<LeftDrawerParamList, T, 'LeftDrawer'>;
```

This isn't the best way to do this, for example, the `id` prop on the navigator itself isn't type-safe. But supporting it with a good API is a lot of work, so for now, I've decided to go with this alternative approach.
  • Loading branch information
satya164 committed Apr 1, 2022
1 parent 76547c1 commit 4e4935a
Show file tree
Hide file tree
Showing 22 changed files with 348 additions and 142 deletions.
11 changes: 9 additions & 2 deletions example/types.check.ts → example/types.check.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type HomeDrawerParamList = {

type HomeDrawerScreenProps<T extends keyof HomeDrawerParamList> =
CompositeScreenProps<
DrawerScreenProps<HomeDrawerParamList, T>,
DrawerScreenProps<HomeDrawerParamList, T, 'LeftDrawer'>,
RootStackScreenProps<keyof RootStackParamList>
>;

Expand All @@ -38,7 +38,7 @@ type FeedTabParamList = {

type FeedTabScreenProps<T extends keyof FeedTabParamList> =
CompositeScreenProps<
BottomTabScreenProps<FeedTabParamList, T>,
BottomTabScreenProps<FeedTabParamList, T, 'BottomTabs'>,
HomeDrawerScreenProps<keyof HomeDrawerParamList>
>;

Expand Down Expand Up @@ -95,6 +95,7 @@ export const PostDetailsScreen = ({
});

expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>();
expectTypeOf(navigation.getParent).parameter(0).toEqualTypeOf<undefined>();
};

export const FeedScreen = ({
Expand Down Expand Up @@ -124,6 +125,9 @@ export const FeedScreen = ({
>();

expectTypeOf(navigation.getState().type).toEqualTypeOf<'drawer'>();
expectTypeOf(navigation.getParent)
.parameter(0)
.toEqualTypeOf<'LeftDrawer' | undefined>();
};

export const PopularScreen = ({
Expand Down Expand Up @@ -153,4 +157,7 @@ export const PopularScreen = ({
>();

expectTypeOf(navigation.getState().type).toEqualTypeOf<'tab'>();
expectTypeOf(navigation.getParent)
.parameter(0)
.toEqualTypeOf<'LeftDrawer' | 'BottomTabs' | undefined>();
};
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@
"react": "17.0.1",
"react-dom": "17.0.1",
"react-native": "0.64.3",
"react-native-web": "~0.17.1",
"tsd": "^0.19.1"
"react-native-web": "~0.17.1"
},
"devDependencies": {
"@babel/core": "^7.12.9",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Props = DefaultNavigatorOptions<
BottomTabNavigationConfig;

function BottomTabNavigator({
id,
initialRouteName,
backBehavior,
children,
Expand Down Expand Up @@ -103,6 +104,7 @@ function BottomTabNavigator({
BottomTabNavigationOptions,
BottomTabNavigationEventMap
>(TabRouter, {
id,
initialRouteName,
backBehavior,
children,
Expand Down
9 changes: 6 additions & 3 deletions packages/bottom-tabs/src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ export type BottomTabNavigationHelpers = NavigationHelpers<

export type BottomTabNavigationProp<
ParamList extends ParamListBase,
RouteName extends keyof ParamList = keyof ParamList
RouteName extends keyof ParamList = keyof ParamList,
NavigatorID extends string | undefined = undefined
> = NavigationProp<
ParamList,
RouteName,
NavigatorID,
TabNavigationState<ParamList>,
BottomTabNavigationOptions,
BottomTabNavigationEventMap
Expand All @@ -54,9 +56,10 @@ export type BottomTabNavigationProp<

export type BottomTabScreenProps<
ParamList extends ParamListBase,
RouteName extends keyof ParamList = keyof ParamList
RouteName extends keyof ParamList = keyof ParamList,
NavigatorID extends string | undefined = undefined
> = {
navigation: BottomTabNavigationProp<ParamList, RouteName>;
navigation: BottomTabNavigationProp<ParamList, RouteName, NavigatorID>;
route: RouteProp<ParamList, RouteName>;
};

Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/SceneView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ import useOptionsGetters from './useOptionsGetters';

type Props<State extends NavigationState, ScreenOptions extends {}> = {
screen: RouteConfigComponent<ParamListBase, string> & { name: string };
navigation: NavigationProp<ParamListBase, string, State, ScreenOptions>;
navigation: NavigationProp<
ParamListBase,
string,
string | undefined,
State,
ScreenOptions
>;
route: Route<string>;
routeState: NavigationState | PartialState<NavigationState> | undefined;
getState: () => State;
Expand Down
158 changes: 158 additions & 0 deletions packages/core/src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1344,6 +1344,164 @@ it('resets state of a nested child in a navigator', () => {
});
});

it('gets immediate parent with getParent()', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);

return descriptors[state.routes[state.index].key].render();
};

const TestComponent = ({ route, navigation }: any): any =>
`${route.name} [${navigation
.getParent()
.getState()
.routes.map((r: any) => r.name)
.join()}]`;

const onStateChange = jest.fn();

const element = render(
<BaseNavigationContainer onStateChange={onStateChange}>
<TestNavigator>
<Screen name="foo">
{() => (
<TestNavigator>
<Screen name="foo-a">
{() => (
<TestNavigator>
<Screen name="bar" component={TestComponent} />
</TestNavigator>
)}
</Screen>
</TestNavigator>
)}
</Screen>
</TestNavigator>
</BaseNavigationContainer>
);

expect(element).toMatchInlineSnapshot(`"bar [foo-a]"`);
});

it('gets parent with a ID with getParent(id)', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);

return descriptors[state.routes[state.index].key].render();
};

const TestComponent = ({ route, navigation }: any): any =>
`${route.name} [${navigation
.getParent('Test')
.getState()
.routes.map((r: any) => r.name)
.join()}]`;

const onStateChange = jest.fn();

const element = render(
<BaseNavigationContainer onStateChange={onStateChange}>
<TestNavigator id="Test">
<Screen name="foo">
{() => (
<TestNavigator>
<Screen name="foo-a">
{() => (
<TestNavigator>
<Screen name="bar" component={TestComponent} />
</TestNavigator>
)}
</Screen>
</TestNavigator>
)}
</Screen>
</TestNavigator>
</BaseNavigationContainer>
);

expect(element).toMatchInlineSnapshot(`"bar [foo]"`);
});

it('gets self with a ID with getParent(id)', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);

return descriptors[state.routes[state.index].key].render();
};

const TestComponent = ({ route, navigation }: any): any =>
`${route.name} [${navigation
.getParent('Test')
.getState()
.routes.map((r: any) => r.name)
.join()}]`;

const onStateChange = jest.fn();

const element = render(
<BaseNavigationContainer onStateChange={onStateChange}>
<TestNavigator>
<Screen name="foo">
{() => (
<TestNavigator>
<Screen name="foo-a">
{() => (
<TestNavigator id="Test">
<Screen name="bar" component={TestComponent} />
</TestNavigator>
)}
</Screen>
</TestNavigator>
)}
</Screen>
</TestNavigator>
</BaseNavigationContainer>
);

expect(element).toMatchInlineSnapshot(`"bar [bar]"`);
});

it('throws when ID is not found with getParent(id)', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);

return descriptors[state.routes[state.index].key].render();
};

const TestComponent = ({ route, navigation }: any): any =>
`${route.name} [${navigation
.getParent('Tes')
.getState()
.routes.map((r: any) => r.name)
.join()}]`;

const onStateChange = jest.fn();

const element = (
<BaseNavigationContainer onStateChange={onStateChange}>
<TestNavigator>
<Screen name="foo">
{() => (
<TestNavigator id="Test">
<Screen name="foo-a">
{() => (
<TestNavigator>
<Screen name="bar" component={TestComponent} />
</TestNavigator>
)}
</Screen>
</TestNavigator>
)}
</Screen>
</TestNavigator>
</BaseNavigationContainer>
);

expect(() => render(element)).toThrowError(
`Couldn't find a parent navigator with the ID "Tes". Is this navigator nested under another navigator with this ID?`
);
});

it('gives access to internal state', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
Expand Down
Loading

0 comments on commit 4e4935a

Please sign in to comment.