Skip to content

Commit 4483dd2

Browse files
authored
feat: make NAVIGATE and JUMP_TO to support key and name of the route (facebook#16)
1 parent 44b2ace commit 4483dd2

File tree

5 files changed

+173
-39
lines changed

5 files changed

+173
-39
lines changed

example/StackNavigator.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,25 +151,42 @@ const StackRouter: Router<CommonAction | Action> = {
151151
});
152152

153153
case 'NAVIGATE':
154-
if (state.routeNames.includes(action.payload.name)) {
154+
if (
155+
action.payload.key ||
156+
(action.payload.name &&
157+
state.routeNames.includes(action.payload.name))
158+
) {
155159
// If the route already exists, navigate to that
156160
let index = -1;
157161

158-
if (state.routes[state.index].name === action.payload.name) {
162+
if (
163+
state.routes[state.index].name === action.payload.name ||
164+
state.routes[state.index].key === action.payload.key
165+
) {
159166
index = state.index;
160167
} else {
161168
for (let i = state.routes.length - 1; i >= 0; i--) {
162-
if (state.routes[i].name === action.payload.name) {
169+
if (
170+
state.routes[i].name === action.payload.name ||
171+
state.routes[i].key === action.payload.key
172+
) {
163173
index = i;
164174
break;
165175
}
166176
}
167177
}
168178

169-
if (index === -1) {
179+
if (index === -1 && action.payload.key) {
180+
return null;
181+
}
182+
183+
if (index === -1 && action.payload.name !== undefined) {
170184
return StackRouter.getStateForAction(state, {
171185
type: 'PUSH',
172-
payload: action.payload,
186+
payload: {
187+
name: action.payload.name,
188+
params: action.payload.params,
189+
},
173190
});
174191
}
175192

example/TabNavigator.tsx

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
ParamListBase,
1010
Router,
1111
createNavigator,
12+
TargetRoute,
1213
} from '../src/index';
1314

1415
type Props = {
@@ -18,7 +19,7 @@ type Props = {
1819

1920
type Action = {
2021
type: 'JUMP_TO';
21-
payload: { name: string; params?: object };
22+
payload: { name?: string; key?: string; params?: object };
2223
};
2324

2425
export type TabNavigationProp<
@@ -31,10 +32,10 @@ export type TabNavigationProp<
3132
* @param name Name of the route for the tab.
3233
* @param [params] Params object for the route.
3334
*/
34-
jumpTo<RouteName extends keyof ParamList>(
35+
jumpTo<RouteName extends Extract<keyof ParamList, string>>(
3536
...args: ParamList[RouteName] extends void
36-
? [RouteName]
37-
: [RouteName, ParamList[RouteName]]
37+
? [TargetRoute<RouteName>]
38+
: [TargetRoute<RouteName>, ParamList[RouteName]]
3839
): void;
3940
};
4041

@@ -98,35 +99,44 @@ const TabRouter: Router<Action | CommonAction> = {
9899
},
99100

100101
getStateForAction(state, action) {
102+
let index = -1;
101103
switch (action.type) {
102104
case 'JUMP_TO':
103105
case 'NAVIGATE':
104-
if (state.routeNames.includes(action.payload.name)) {
105-
const index = state.routes.findIndex(
106+
if (action.payload.key) {
107+
index = state.routes.findIndex(
108+
route => route.key === action.payload.key
109+
);
110+
}
111+
112+
if (action.payload.name) {
113+
index = state.routes.findIndex(
106114
route => route.name === action.payload.name
107115
);
116+
}
108117

109-
return {
110-
...state,
111-
routes:
112-
action.payload.params !== undefined
113-
? state.routes.map((route, i) =>
114-
i === index
115-
? {
116-
...route,
117-
params: {
118-
...route.params,
119-
...action.payload.params,
120-
},
121-
}
122-
: route
123-
)
124-
: state.routes,
125-
index,
126-
};
118+
if (index == -1) {
119+
return null;
127120
}
128121

129-
return null;
122+
return {
123+
...state,
124+
routes:
125+
action.payload.params !== undefined
126+
? state.routes.map((route, i) =>
127+
i === index
128+
? {
129+
...route,
130+
params: {
131+
...route.params,
132+
...action.payload.params,
133+
},
134+
}
135+
: route
136+
)
137+
: state.routes,
138+
index,
139+
};
130140

131141
case 'REPLACE': {
132142
return {
@@ -171,8 +181,20 @@ const TabRouter: Router<Action | CommonAction> = {
171181
},
172182

173183
actionCreators: {
174-
jumpTo(name: string, params?: object) {
175-
return { type: 'JUMP_TO', payload: { name, params } };
184+
jumpTo(target: TargetRoute<string>, params?: object) {
185+
if (typeof target === 'string') {
186+
return { type: 'JUMP_TO', payload: { name: target, params } };
187+
} else {
188+
if (
189+
(target.hasOwnProperty('key') && target.hasOwnProperty('name')) ||
190+
(!target.hasOwnProperty('key') && !target.hasOwnProperty('name'))
191+
) {
192+
throw new Error(
193+
'While calling jumpTo you need to specify either name or key'
194+
);
195+
}
196+
return { type: 'JUMP_TO', payload: { ...target, params } };
197+
}
176198
},
177199
},
178200
};

src/BaseActions.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { PartialState } from './types';
1+
import { PartialState, TargetRoute } from './types';
22

33
export type Action =
44
| { type: 'GO_BACK' }
55
| {
66
type: 'NAVIGATE';
7-
payload: { name: string; params?: object };
7+
payload: { name?: string; key?: string; params?: object };
88
}
99
| {
1010
type: 'REPLACE';
@@ -19,8 +19,20 @@ export function goBack(): Action {
1919
return { type: 'GO_BACK' };
2020
}
2121

22-
export function navigate(name: string, params?: object): Action {
23-
return { type: 'NAVIGATE', payload: { name, params } };
22+
export function navigate(target: TargetRoute<string>, params?: object): Action {
23+
if (
24+
(target.hasOwnProperty('key') && target.hasOwnProperty('name')) ||
25+
(!target.hasOwnProperty('key') && !target.hasOwnProperty('name'))
26+
) {
27+
throw new Error(
28+
'While calling navigate you need to specify either name or key'
29+
);
30+
}
31+
if (typeof target === 'string') {
32+
return { type: 'NAVIGATE', payload: { name: target, params } };
33+
} else {
34+
return { type: 'NAVIGATE', payload: { ...target, params } };
35+
}
2436
}
2537

2638
export function replace(name: string, params?: object): Action {

src/__tests__/BaseActions.test.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import * as React from 'react';
2+
import { render } from 'react-native-testing-library';
3+
import Screen from '../Screen';
4+
import NavigationContainer from '../NavigationContainer';
5+
import useNavigationBuilder from '../useNavigationBuilder';
6+
import MockRouter from './__fixtures__/MockRouter';
7+
8+
beforeEach(() => (MockRouter.key = 0));
9+
10+
it('throws if NAVIGATE dispatched with both key and name', () => {
11+
const TestNavigator = (props: any) => {
12+
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
13+
14+
return descriptors[state.routes[state.index].key].render();
15+
};
16+
17+
const FooScreen = (props: any) => {
18+
React.useEffect(() => {
19+
props.navigation.navigate({ key: '1', name: '2' });
20+
// eslint-disable-next-line react-hooks/exhaustive-deps
21+
}, []);
22+
23+
return null;
24+
};
25+
26+
const onStateChange = jest.fn();
27+
28+
const element = (
29+
<NavigationContainer onStateChange={onStateChange}>
30+
<TestNavigator initialRouteName="foo">
31+
<Screen
32+
name="foo"
33+
component={FooScreen}
34+
initialParams={{ count: 10 }}
35+
/>
36+
</TestNavigator>
37+
</NavigationContainer>
38+
);
39+
40+
expect(() => render(element).update(element)).toThrowError(
41+
'While calling navigate you need to specify either name or key'
42+
);
43+
});
44+
45+
it('throws if NAVIGATE dispatched neither both key nor name', () => {
46+
const TestNavigator = (props: any) => {
47+
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
48+
49+
return descriptors[state.routes[state.index].key].render();
50+
};
51+
52+
const FooScreen = (props: any) => {
53+
React.useEffect(() => {
54+
props.navigation.navigate({});
55+
// eslint-disable-next-line react-hooks/exhaustive-deps
56+
}, []);
57+
58+
return null;
59+
};
60+
61+
const onStateChange = jest.fn();
62+
63+
const element = (
64+
<NavigationContainer onStateChange={onStateChange}>
65+
<TestNavigator initialRouteName="foo">
66+
<Screen
67+
name="foo"
68+
component={FooScreen}
69+
initialParams={{ count: 10 }}
70+
/>
71+
</TestNavigator>
72+
</NavigationContainer>
73+
);
74+
75+
expect(() => render(element).update(element)).toThrowError(
76+
'While calling navigate you need to specify either name or key'
77+
);
78+
});

src/types.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import * as BaseActions from './BaseActions';
22

33
export type CommonAction = BaseActions.Action;
44

5+
export type TargetRoute<RouteName extends string> =
6+
| RouteName
7+
| { name: RouteName }
8+
| { key: string };
9+
510
export type NavigationState = {
611
/**
712
* Unique key for the navigation state.
@@ -163,10 +168,10 @@ export type NavigationHelpers<
163168
* @param name Name of the route to navigate to.
164169
* @param [params] Params object for the route.
165170
*/
166-
navigate<RouteName extends keyof ParamList>(
171+
navigate<RouteName extends Extract<keyof ParamList, string>>(
167172
...args: ParamList[RouteName] extends undefined
168-
? [RouteName] | [RouteName, undefined]
169-
: [RouteName, ParamList[RouteName]]
173+
? [TargetRoute<RouteName>] | [TargetRoute<RouteName>, undefined]
174+
: [TargetRoute<RouteName>, ParamList[RouteName]]
170175
): void;
171176

172177
/**

0 commit comments

Comments
 (0)