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

Commit

Permalink
feat: add hook for deep link support
Browse files Browse the repository at this point in the history
  • Loading branch information
satya164 committed Aug 19, 2019
1 parent f0b80ce commit 35987ae
Show file tree
Hide file tree
Showing 16 changed files with 279 additions and 78 deletions.
6 changes: 3 additions & 3 deletions packages/core/src/BaseActions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NavigationState } from './types';
import { NavigationState, PartialState } from './types';

export type Action =
| {
Expand All @@ -22,7 +22,7 @@ export type Action =
}
| {
type: 'RESET';
payload: Partial<NavigationState>;
payload: PartialState<NavigationState>;
source?: string;
target?: string;
}
Expand Down Expand Up @@ -64,7 +64,7 @@ export function replace(name: string, params?: object): Action {
return { type: 'REPLACE', payload: { name, params } };
}

export function reset(state: Partial<NavigationState>): Action {
export function reset(state: PartialState<NavigationState>): Action {
return { type: 'RESET', payload: state };
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/NavigationBuilderContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const NavigationBuilderContext = React.createContext<{
addActionListener?: (listener: ChildActionListener) => void;
addFocusedListener?: (listener: FocusedNavigationListener) => void;
onRouteFocus?: (key: string) => void;
trackAction: (key: string, action: NavigationAction) => void;
trackAction: (action: NavigationAction) => void;
}>({
trackAction: () => undefined,
});
Expand Down
17 changes: 9 additions & 8 deletions packages/core/src/NavigationContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,18 @@ import {
NavigationState,
InitialState,
PartialState,
ParamListBase,
NavigationHelpers,
NavigationAction,
NavigationContainerRef,
} from './types';

type State = NavigationState | PartialState<NavigationState> | undefined;

type Props = {
initialState?: InitialState;
onStateChange?: (
state: NavigationState | PartialState<NavigationState> | undefined
) => void;
onStateChange?: (state: State) => void;
children: React.ReactNode;
};

type State = NavigationState | PartialState<NavigationState> | undefined;

const MISSING_CONTEXT_ERROR =
"We couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?";

Expand Down Expand Up @@ -88,7 +85,7 @@ const getPartialState = (
*/
const Container = React.forwardRef(function NavigationContainer(
{ initialState, onStateChange, children }: Props,
ref: React.Ref<NavigationHelpers<ParamListBase>>
ref: React.Ref<NavigationContainerRef>
) {
const [state, setNavigationState] = React.useState<State>(() =>
getPartialState(initialState)
Expand Down Expand Up @@ -126,6 +123,10 @@ const Container = React.forwardRef(function NavigationContainer(
);
return acc;
}, {}),
resetRoot: (state: PartialState<NavigationState> | NavigationState) => {
trackAction('@@RESET_ROOT');
setNavigationState(state);
},
dispatch,
canGoBack,
}));
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/__tests__/BaseRouter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ it('resets state to new state with RESET', () => {
it('ignores key and routeNames when resetting with RESET', () => {
const result = BaseRouter.getStateForAction(
STATE,
// @ts-ignore
BaseActions.reset({ index: 2, key: 'foo', routeNames: ['test'] })
);

Expand Down
77 changes: 73 additions & 4 deletions packages/core/src/__tests__/NavigationContainer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import {
DefaultRouterOptions,
NavigationState,
Router,
NavigationHelpers,
ParamListBase,
NavigationContainerRef,
} from '../types';

it('throws when getState is accessed without a container', () => {
Expand Down Expand Up @@ -173,7 +172,7 @@ it('handle dispatching with ref', () => {
);
};

const ref = React.createRef<NavigationHelpers<ParamListBase>>();
const ref = React.createRef<NavigationContainerRef>();

const onStateChange = jest.fn();

Expand Down Expand Up @@ -226,7 +225,7 @@ it('handle dispatching with ref', () => {
render(element).update(element);

act(() => {
if (ref.current !== null) {
if (ref.current != null) {
ref.current.dispatch({ type: 'REVERSE' });
}
});
Expand All @@ -253,3 +252,73 @@ it('handle dispatching with ref', () => {
],
});
});

it('handle resetting state with ref', () => {
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);

return (
<React.Fragment>
{state.routes.map(route => descriptors[route.key].render())}
</React.Fragment>
);
};

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

const onStateChange = jest.fn();

const element = (
<NavigationContainer ref={ref} onStateChange={onStateChange}>
<TestNavigator>
<Screen name="foo">{() => null}</Screen>
<Screen name="foo2">
{() => (
<TestNavigator>
<Screen name="qux" component={() => null} />
<Screen name="lex" component={() => null} />
</TestNavigator>
)}
</Screen>
<Screen name="bar">{() => null}</Screen>
<Screen name="baz">
{() => (
<TestNavigator>
<Screen name="qux" component={() => null} />
<Screen name="lex" component={() => null} />
</TestNavigator>
)}
</Screen>
</TestNavigator>
</NavigationContainer>
);

render(element).update(element);

const state = {
stale: true,
index: 1,
routes: [
{
key: 'baz',
name: 'baz',
state: {
index: 0,
key: '4',
routeNames: ['qux', 'lex'],
routes: [{ key: 'qux', name: 'qux' }, { key: 'lex', name: 'lex' }],
},
},
{ key: 'bar', name: 'bar' },
],
};

act(() => {
if (ref.current != null) {
ref.current.resetRoot(state);
}
});

expect(onStateChange).toBeCalledTimes(1);
expect(onStateChange).lastCalledWith(state);
});
10 changes: 5 additions & 5 deletions packages/core/src/getStateFromPath.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { InitialState } from './types';
import { NavigationState, PartialState } from './types';

/**
* Utility to parse a path string to initial state object accepted by the container.
Expand All @@ -8,17 +8,17 @@ import { InitialState } from './types';
*/
export default function getStateFromPath(
path: string
): InitialState | undefined {
): PartialState<NavigationState> | undefined {
const parts = path.split('?');
const segments = parts[0].split('/').filter(Boolean);
const query = parts[1] ? parts[1].split('&') : undefined;

let result: InitialState | undefined;
let current: InitialState | undefined;
let result: PartialState<NavigationState> | undefined;
let current: PartialState<NavigationState> | undefined;

while (segments.length) {
const state = {
stale: true as true,
stale: true,
routes: [{ name: decodeURIComponent(segments[0]) }],
};

Expand Down
16 changes: 14 additions & 2 deletions packages/core/src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export type InitialState = Partial<
export type PartialState<State extends NavigationState> = Partial<
Omit<State, 'stale' | 'key' | 'routes' | 'routeNames'>
> & {
stale: boolean;
stale?: boolean;
routes: Array<
Omit<Route<string>, 'key'> & { key?: string; state?: InitialState }
>;
Expand Down Expand Up @@ -304,7 +304,7 @@ type NavigationHelpersCommon<
*
* @param state Navigation state object.
*/
reset(state: Partial<State>): void;
reset(state: PartialState<State> | State): void;

/**
* Go back to the previous route in history.
Expand Down Expand Up @@ -486,6 +486,18 @@ export type RouteConfig<
children: (props: any) => React.ReactNode;
});

export type NavigationContainerRef =
| NavigationHelpers<ParamListBase> & {
/**
* Reset the navigation state of the root navigator to the provided state.
*
* @param state Navigation state object.
*/
resetRoot(state: PartialState<NavigationState> | NavigationState): void;
}
| undefined
| null;

export type TypedNavigator<
ParamList extends ParamListBase,
ScreenOptions extends object,
Expand Down
8 changes: 3 additions & 5 deletions packages/core/src/useDevTools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,7 @@ export default function useDevTools({ name, reset, state }: Options) {

const devTools = devToolsRef.current;
const lastStateRef = React.useRef<State>(state);
const actions = React.useRef<
Array<{ key: string; action: NavigationAction }>
>([]);
const actions = React.useRef<Array<NavigationAction | string>>([]);

React.useEffect(() => {
devTools && devTools.init(lastStateRef.current);
Expand Down Expand Up @@ -84,12 +82,12 @@ export default function useDevTools({ name, reset, state }: Options) {
);

const trackAction = React.useCallback(
(key: string, action: NavigationAction) => {
(action: NavigationAction | string) => {
if (!devTools) {
return;
}

actions.current.push({ key, action });
actions.current.push(action);
},
[devTools]
);
Expand Down
42 changes: 28 additions & 14 deletions packages/core/src/useNavigationBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,27 +129,41 @@ export default function useNavigationBuilder<
performTransaction,
} = React.useContext(NavigationStateContext);

const [initialState] = React.useState(() =>
const previousStateRef = React.useRef<
NavigationState | PartialState<NavigationState> | undefined
>();
const initializedStateRef = React.useRef<State>();

if (
initializedStateRef.current === undefined ||
currentState !== previousStateRef.current
) {
// If the current state isn't initialized on first render, we initialize it
// We also need to re-initialize it if the state passed from parent was changed (maybe due to reset)
// Otherwise assume that the state was provided as initial state
// So we need to rehydrate it to make it usable
currentState === undefined
? router.getInitialState({
routeNames,
routeParamList,
})
: router.getRehydratedState(currentState as PartialState<State>, {
routeNames,
routeParamList,
})
);
initializedStateRef.current =
currentState === undefined
? router.getInitialState({
routeNames,
routeParamList,
})
: router.getRehydratedState(currentState as PartialState<State>, {
routeNames,
routeParamList,
});
}

React.useEffect(() => {
previousStateRef.current = currentState;
}, [currentState]);

let state =
// If the state isn't initialized, or stale, use the state we initialized instead
// The state won't update until there's a change needed in the state we have initalized locally
// So it'll be `undefined` or stale untill the first navigation event happens
currentState === undefined || currentState.stale
? initialState
? (initializedStateRef.current as State)
: (currentState as State);

if (!isArrayEqual(state.routeNames, routeNames)) {
Expand Down Expand Up @@ -188,9 +202,9 @@ export default function useNavigationBuilder<
const currentState = getCurrentState();

return currentState === undefined || currentState.stale
? initialState
? (initializedStateRef.current as State)
: (currentState as State);
}, [getCurrentState, initialState]);
}, [getCurrentState]);

const emitter = useEventEmitter();

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/useOnAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default function useOnAction({
result = result === null && action.target === state.key ? state : result;

if (result !== null) {
trackAction(state.key, action);
trackAction(action);

if (state !== result) {
setState(result);
Expand Down
Loading

0 comments on commit 35987ae

Please sign in to comment.