Skip to content
This repository has been archived by the owner on Feb 8, 2020. It is now read-only.

feat: make NAVIGATE and JUMP_TO to support key and name of the route #16

Merged
merged 3 commits into from
Jul 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions example/StackNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,25 +151,42 @@ const StackRouter: Router<CommonAction | Action> = {
});

case 'NAVIGATE':
if (state.routeNames.includes(action.payload.name)) {
if (
action.payload.key ||
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should also check if the key exists in the stack

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we do it line 180

(action.payload.name &&
state.routeNames.includes(action.payload.name))
) {
// If the route already exists, navigate to that
let index = -1;

if (state.routes[state.index].name === action.payload.name) {
if (
state.routes[state.index].name === action.payload.name ||
state.routes[state.index].key === action.payload.key
) {
index = state.index;
} else {
for (let i = state.routes.length - 1; i >= 0; i--) {
if (state.routes[i].name === action.payload.name) {
if (
state.routes[i].name === action.payload.name ||
state.routes[i].key === action.payload.key
) {
index = i;
break;
}
}
}

if (index === -1) {
if (index === -1 && action.payload.key) {
return null;
}

if (index === -1 && action.payload.name !== undefined) {
return StackRouter.getStateForAction(state, {
type: 'PUSH',
payload: action.payload,
payload: {
name: action.payload.name,
params: action.payload.params,
},
});
}

Expand Down
76 changes: 49 additions & 27 deletions example/TabNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ParamListBase,
Router,
createNavigator,
TargetRoute,
} from '../src/index';

type Props = {
Expand All @@ -18,7 +19,7 @@ type Props = {

type Action = {
type: 'JUMP_TO';
payload: { name: string; params?: object };
payload: { name?: string; key?: string; params?: object };
};

export type TabNavigationProp<
Expand All @@ -31,10 +32,10 @@ export type TabNavigationProp<
* @param name Name of the route for the tab.
* @param [params] Params object for the route.
*/
jumpTo<RouteName extends keyof ParamList>(
jumpTo<RouteName extends Extract<keyof ParamList, string>>(
...args: ParamList[RouteName] extends void
? [RouteName]
: [RouteName, ParamList[RouteName]]
? [TargetRoute<RouteName>]
: [TargetRoute<RouteName>, ParamList[RouteName]]
): void;
};

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

getStateForAction(state, action) {
let index = -1;
switch (action.type) {
case 'JUMP_TO':
case 'NAVIGATE':
if (state.routeNames.includes(action.payload.name)) {
const index = state.routes.findIndex(
if (action.payload.key) {
index = state.routes.findIndex(
route => route.key === action.payload.key
);
}

if (action.payload.name) {
index = state.routes.findIndex(
route => route.name === action.payload.name
);
}

return {
...state,
routes:
action.payload.params !== undefined
? state.routes.map((route, i) =>
i === index
? {
...route,
params: {
...route.params,
...action.payload.params,
},
}
: route
)
: state.routes,
index,
};
if (index == -1) {
return null;
}

return null;
return {
...state,
routes:
action.payload.params !== undefined
? state.routes.map((route, i) =>
i === index
? {
...route,
params: {
...route.params,
...action.payload.params,
},
}
: route
)
: state.routes,
index,
};

case 'REPLACE': {
return {
Expand Down Expand Up @@ -171,8 +181,20 @@ const TabRouter: Router<Action | CommonAction> = {
},

actionCreators: {
jumpTo(name: string, params?: object) {
return { type: 'JUMP_TO', payload: { name, params } };
jumpTo(target: TargetRoute<string>, params?: object) {
if (typeof target === 'string') {
return { type: 'JUMP_TO', payload: { name: target, params } };
} else {
if (
(target.hasOwnProperty('key') && target.hasOwnProperty('name')) ||
(!target.hasOwnProperty('key') && !target.hasOwnProperty('name'))
) {
throw new Error(
'While calling jumpTo you need to specify either name or key'
);
}
return { type: 'JUMP_TO', payload: { ...target, params } };
}
},
},
};
Expand Down
20 changes: 16 additions & 4 deletions src/BaseActions.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { PartialState } from './types';
import { PartialState, TargetRoute } from './types';

export type Action =
| { type: 'GO_BACK' }
| {
type: 'NAVIGATE';
payload: { name: string; params?: object };
payload: { name?: string; key?: string; params?: object };
}
| {
type: 'REPLACE';
Expand All @@ -19,8 +19,20 @@ export function goBack(): Action {
return { type: 'GO_BACK' };
}

export function navigate(name: string, params?: object): Action {
return { type: 'NAVIGATE', payload: { name, params } };
export function navigate(target: TargetRoute<string>, params?: object): Action {
if (
(target.hasOwnProperty('key') && target.hasOwnProperty('name')) ||
(!target.hasOwnProperty('key') && !target.hasOwnProperty('name'))
) {
throw new Error(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe instead of throwing in the action creator, we can throw in the router. Then it'll also prevent someone from calling dispatch directly with incorrect stuff

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. We don't want anyone to dispatch NAVIGATE directly. If yes, risk included. I don't want to copy this error to every new and existing navigators.

'While calling navigate you need to specify either name or key'
);
}
if (typeof target === 'string') {
return { type: 'NAVIGATE', payload: { name: target, params } };
} else {
return { type: 'NAVIGATE', payload: { ...target, params } };
}
}

export function replace(name: string, params?: object): Action {
Expand Down
78 changes: 78 additions & 0 deletions src/__tests__/BaseActions.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import * as React from 'react';
import { render } from 'react-native-testing-library';
import Screen from '../Screen';
import NavigationContainer from '../NavigationContainer';
import useNavigationBuilder from '../useNavigationBuilder';
import MockRouter from './__fixtures__/MockRouter';

beforeEach(() => (MockRouter.key = 0));

it('throws if NAVIGATE dispatched with both key and name', () => {
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);

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

const FooScreen = (props: any) => {
React.useEffect(() => {
props.navigation.navigate({ key: '1', name: '2' });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return null;
};

const onStateChange = jest.fn();

const element = (
<NavigationContainer onStateChange={onStateChange}>
<TestNavigator initialRouteName="foo">
<Screen
name="foo"
component={FooScreen}
initialParams={{ count: 10 }}
/>
</TestNavigator>
</NavigationContainer>
);

expect(() => render(element).update(element)).toThrowError(
'While calling navigate you need to specify either name or key'
);
});

it('throws if NAVIGATE dispatched neither both key nor name', () => {
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);

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

const FooScreen = (props: any) => {
React.useEffect(() => {
props.navigation.navigate({});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return null;
};

const onStateChange = jest.fn();

const element = (
<NavigationContainer onStateChange={onStateChange}>
<TestNavigator initialRouteName="foo">
<Screen
name="foo"
component={FooScreen}
initialParams={{ count: 10 }}
/>
</TestNavigator>
</NavigationContainer>
);

expect(() => render(element).update(element)).toThrowError(
'While calling navigate you need to specify either name or key'
);
});
11 changes: 8 additions & 3 deletions src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import * as BaseActions from './BaseActions';

export type CommonAction = BaseActions.Action;

export type TargetRoute<RouteName extends string> =
| RouteName
| { name: RouteName }
| { key: string };

export type NavigationState = {
/**
* Unique key for the navigation state.
Expand Down Expand Up @@ -163,10 +168,10 @@ export type NavigationHelpers<
* @param name Name of the route to navigate to.
* @param [params] Params object for the route.
*/
navigate<RouteName extends keyof ParamList>(
navigate<RouteName extends Extract<keyof ParamList, string>>(
...args: ParamList[RouteName] extends undefined
? [RouteName] | [RouteName, undefined]
: [RouteName, ParamList[RouteName]]
? [TargetRoute<RouteName>] | [TargetRoute<RouteName>, undefined]
: [TargetRoute<RouteName>, ParamList[RouteName]]
): void;

/**
Expand Down