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

Commit

Permalink
feat: make it easier to navigate to a specific route in navigator
Browse files Browse the repository at this point in the history
We now use the `params` of the route to determine `initialRouteName` and `initialParams` of a navigator.

So you can do something like this:

```js
navigation.push('Auth', { name: 'Login', params: { token 'xxx' } })
```

This will navigate to the `Login` screen inside the `Auth` navigator.

Closes #90
  • Loading branch information
satya164 committed Sep 25, 2019
1 parent 8b78d61 commit 9a0cbd0
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 4 deletions.
19 changes: 18 additions & 1 deletion packages/core/src/__tests__/__fixtures__/MockRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,24 @@ export default function MockRouter(options: DefaultRouterOptions) {
return null;
}

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

default:
Expand Down
76 changes: 75 additions & 1 deletion packages/core/src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import NavigationContainer from '../NavigationContainer';
import useNavigationBuilder from '../useNavigationBuilder';
import useNavigation from '../useNavigation';
import MockRouter, { MockRouterKey } from './__fixtures__/MockRouter';
import { NavigationState } from '../types';
import { NavigationState, NavigationContainerRef } from '../types';

beforeEach(() => (MockRouterKey.current = 0));

Expand Down Expand Up @@ -523,6 +523,80 @@ it('handles change in route names', () => {
});
});

it('navigates to nested child in a navigator', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);

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

const TestComponent = ({ route }: any): any =>
`[${route.name}, ${JSON.stringify(route.params)}]`;

const onStateChange = jest.fn();

const navigation = React.createRef<NavigationContainerRef>();

const element = render(
<NavigationContainer ref={navigation} onStateChange={onStateChange}>
<TestNavigator>
<Screen name="foo">
{() => (
<TestNavigator>
<Screen name="foo-a" component={TestComponent} />
<Screen name="foo-b" component={TestComponent} />
</TestNavigator>
)}
</Screen>
<Screen name="bar">
{() => (
<TestNavigator initialRouteName="bar-a">
<Screen
name="bar-a"
component={TestComponent}
initialParams={{ lol: 'why' }}
/>
<Screen
name="bar-b"
component={TestComponent}
initialParams={{ some: 'stuff' }}
/>
</TestNavigator>
)}
</Screen>
</TestNavigator>
</NavigationContainer>
);

expect(element).toMatchInlineSnapshot(`"[foo-a, undefined]"`);

act(
() =>
navigation.current &&
navigation.current.navigate('bar', {
name: 'bar-b',
params: { test: 42 },
})
);

expect(element).toMatchInlineSnapshot(
`"[bar-b, {\\"some\\":\\"stuff\\",\\"test\\":42}]"`
);

act(
() =>
navigation.current &&
navigation.current.navigate('bar', {
name: 'bar-a',
params: { whoa: 'test' },
})
);

expect(element).toMatchInlineSnapshot(
`"[bar-a, {\\"lol\\":\\"why\\",\\"whoa\\":\\"test\\"}]"`
);
});

it('gives access to internal state', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
Expand Down
69 changes: 67 additions & 2 deletions packages/core/src/useNavigationBuilder.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as React from 'react';
import { NavigationStateContext } from './NavigationContainer';
import NavigationRouteContext from './NavigationRouteContext';
import Screen from './Screen';
import { navigate } from './CommonActions';
import useEventEmitter from './useEventEmitter';
import useRegisterNavigator from './useRegisterNavigator';
import useDescriptors from './useDescriptors';
Expand Down Expand Up @@ -28,6 +30,13 @@ import {
// eslint-disable-next-line babel/no-unused-expressions
PrivateValueStore;

type NavigatorRoute = {
params?: {
name?: string;
params?: object;
};
};

/**
* Compare two arrays with primitive values as the content.
* We need to make sure that both values and order match.
Expand Down Expand Up @@ -94,9 +103,18 @@ export default function useNavigationBuilder<
) {
useRegisterNavigator();

const route = React.useContext(NavigationRouteContext) as (
| NavigatorRoute
| undefined);

const { children, ...rest } = options;
const { current: router } = React.useRef<Router<State, any>>(
createRouter((rest as unknown) as RouterOptions)
createRouter({
...((rest as unknown) as RouterOptions),
...(route && route.params && typeof route.params.name === 'string'
? { initialRouteName: route.params.name }
: null),
})
);

const screens = getRouteConfigsFromChildren<ScreenOptions>(children).reduce(
Expand All @@ -116,7 +134,20 @@ export default function useNavigationBuilder<
const routeNames = Object.keys(screens);
const routeParamList = routeNames.reduce(
(acc, curr) => {
acc[curr] = screens[curr].initialParams;
const { initialParams } = screens[curr];
const initialParamsFromParams =
route && route.params && route.params.name === curr
? route.params.params
: undefined;

acc[curr] =
initialParams !== undefined || initialParamsFromParams !== undefined
? {
...initialParams,
...initialParamsFromParams,
}
: undefined;

return acc;
},
{} as { [key: string]: object | undefined }
Expand Down Expand Up @@ -213,6 +244,36 @@ export default function useNavigationBuilder<
: (currentState as State);
}, [getCurrentState]);

const previousRouteRef = React.useRef(route);
const previousRoute = previousRouteRef.current;

// If there wasn't a previous route, there won't ever be route object
// So just checking if both exist is enough for us
if (
previousRoute &&
route &&
route.params &&
typeof route.params.name === 'string' &&
route.params !== previousRoute.params
) {
// If the route was updated with new name and/or params, we should navigate there
// The update should be limited to current navigator only, so we call the router manually
const nextState = router.getStateForAction(
state,
navigate(route.params.name, route.params.params)
);

if (nextState !== null) {
if (state !== nextState) {
performTransaction(() => {
setState(nextState);
});
}

state = nextState;
}
}

const emitter = useEventEmitter();

useFocusEvents({ state, emitter });
Expand Down Expand Up @@ -269,6 +330,10 @@ export default function useNavigationBuilder<
emitter,
});

React.useEffect(() => {
previousRouteRef.current = route;
}, [route]);

return {
state,
navigation,
Expand Down

0 comments on commit 9a0cbd0

Please sign in to comment.