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 (#114)
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 authored Oct 18, 2019
1 parent cd5f355 commit a543f1b
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 17 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', {
screen: 'bar-b',
params: { test: 42 },
})
);

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

act(
() =>
navigation.current &&
navigation.current.navigate('bar', {
screen: '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
86 changes: 71 additions & 15 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 @@ -30,6 +32,13 @@ import useOnGetState from './useOnGetState';
// eslint-disable-next-line babel/no-unused-expressions
PrivateValueStore;

type NavigatorRoute = {
params?: {
screen?: 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 @@ -96,9 +105,24 @@ export default function useNavigationBuilder<
) {
useRegisterNavigator();

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

const previousRouteRef = React.useRef(route);

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

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.screen === 'string'
? { initialRouteName: route.params.screen }
: null),
})
);

const screens = getRouteConfigsFromChildren<ScreenOptions>(children).reduce(
Expand All @@ -118,7 +142,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.screen === 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 @@ -175,28 +212,47 @@ export default function useNavigationBuilder<
? (initializedStateRef.current as State)
: (currentState as State);

let nextState: State = state;

if (!isArrayEqual(state.routeNames, routeNames)) {
// When the list of route names change, the router should handle it to remove invalid routes
const nextState = router.getStateForRouteNamesChange(state, {
nextState = router.getStateForRouteNamesChange(state, {
routeNames,
routeParamList,
});
}

if (state !== nextState) {
// If the state needs to be updated, we'll schedule an update with React
// setState in render seems hacky, but that's how React docs implement getDerivedPropsFromState
// https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops
performTransaction(() => {
setState(nextState);
});
}
if (
previousRouteRef.current &&
route &&
route.params &&
typeof route.params.screen === 'string' &&
route.params !== previousRouteRef.current.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 updatedState = router.getStateForAction(
state,
navigate(route.params.screen, route.params.params)
);

// The up-to-date state will come in next render, but we don't need to wait for it
// We can't use the outdated state since the screens have changed, which will cause error due to mismatched config
// So we override the state objec we return to use the latest state as soon as possible
state = nextState;
nextState = updatedState !== null ? updatedState : state;
}

if (state !== nextState) {
// If the state needs to be updated, we'll schedule an update with React
// setState in render seems hacky, but that's how React docs implement getDerivedPropsFromState
// https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops
performTransaction(() => {
setState(nextState);
});
}

// The up-to-date state will come in next render, but we don't need to wait for it
// We can't use the outdated state since the screens have changed, which will cause error due to mismatched config
// So we override the state objec we return to use the latest state as soon as possible
state = nextState;

React.useEffect(() => {
return () => {
// We need to clean up state for this navigator on unmount
Expand Down

0 comments on commit a543f1b

Please sign in to comment.