StackRouter block actions while transitioning (#3469)

The most straightforward fix for two issues is to block all navigation actions while mid-transition of a stack navigator. This will fix:

The double-navigate on double tap issue, because the first navigation will start the transition and the second action will be ignored.

Will fix the buggy header experience that you can see when going back and forward to a different route quickly. This happens because the next navigate action happens before the completion action. After the fix, the navigate action will be ignored, the user will tap again, and will see a good transition
This commit is contained in:
Eric Vicenti 2018-02-08 09:02:47 -08:00 committed by Brent Vatne
parent cf36f22e68
commit 858a0d7a53
4 changed files with 48 additions and 21 deletions

View File

@ -45,6 +45,9 @@ const navigate = createAction(NAVIGATE, payload => {
if (payload.key) { if (payload.key) {
action.key = payload.key; action.key = payload.key;
} }
if (payload.immediate) {
action.immediate = payload.immediate;
}
return action; return action;
}); });
@ -70,6 +73,9 @@ const push = createAction(PUSH, payload => {
if (payload.action) { if (payload.action) {
action.action = payload.action; action.action = payload.action;
} }
if (payload.immediate) {
action.immediate = payload.immediate;
}
return action; return action;
}); });

View File

@ -109,7 +109,7 @@ describe('NavigationContainer', () => {
// First dispatch // First dispatch
expect( expect(
navigationContainer.dispatch( navigationContainer.dispatch(
NavigationActions.navigate({ routeName: 'bar' }) NavigationActions.navigate({ routeName: 'bar', immediate: true })
) )
).toEqual(true); ).toEqual(true);
@ -119,7 +119,7 @@ describe('NavigationContainer', () => {
// Second dispatch // Second dispatch
expect( expect(
navigationContainer.dispatch( navigationContainer.dispatch(
NavigationActions.navigate({ routeName: 'baz' }) NavigationActions.navigate({ routeName: 'baz', immediate: true })
) )
).toEqual(true); ).toEqual(true);
@ -147,7 +147,7 @@ describe('NavigationContainer', () => {
// First dispatch // First dispatch
expect( expect(
navigationContainer.dispatch( navigationContainer.dispatch(
NavigationActions.navigate({ routeName: 'bar' }) NavigationActions.navigate({ routeName: 'bar', immediate: true })
) )
).toEqual(true); ).toEqual(true);
@ -157,28 +157,28 @@ describe('NavigationContainer', () => {
// Second dispatch // Second dispatch
expect( expect(
navigationContainer.dispatch( navigationContainer.dispatch(
NavigationActions.navigate({ routeName: 'baz' }) NavigationActions.navigate({ routeName: 'baz', immediate: true })
) )
).toEqual(true); ).toEqual(true);
// Third dispatch // Third dispatch
expect( expect(
navigationContainer.dispatch( navigationContainer.dispatch(
NavigationActions.navigate({ routeName: 'car' }) NavigationActions.navigate({ routeName: 'car', immediate: true })
) )
).toEqual(true); ).toEqual(true);
// Fourth dispatch // Fourth dispatch
expect( expect(
navigationContainer.dispatch( navigationContainer.dispatch(
NavigationActions.navigate({ routeName: 'dog' }) NavigationActions.navigate({ routeName: 'dog', immediate: true })
) )
).toEqual(true); ).toEqual(true);
// Fifth dispatch // Fifth dispatch
expect( expect(
navigationContainer.dispatch( navigationContainer.dispatch(
NavigationActions.navigate({ routeName: 'elk' }) NavigationActions.navigate({ routeName: 'elk', immediate: true })
) )
).toEqual(true); ).toEqual(true);

View File

@ -148,6 +148,17 @@ export default (routeConfigs, stackConfig = {}) => {
}; };
} }
if (
action.type === NavigationActions.COMPLETE_TRANSITION &&
(action.key == null || action.key === state.key) &&
state.isTransitioning
) {
return {
...state,
isTransitioning: false,
};
}
// Check if a child scene wants to handle the action as long as it is not a reset to the root stack // Check if a child scene wants to handle the action as long as it is not a reset to the root stack
if (action.type !== NavigationActions.RESET || action.key !== null) { if (action.type !== NavigationActions.RESET || action.key !== null) {
const keyIndex = action.key const keyIndex = action.key
@ -218,6 +229,10 @@ export default (routeConfigs, stackConfig = {}) => {
behavesLikePushAction(action) && behavesLikePushAction(action) &&
childRouters[action.routeName] !== undefined childRouters[action.routeName] !== undefined
) { ) {
if (state.isTransitioning) {
return { ...state };
}
const childRouter = childRouters[action.routeName]; const childRouter = childRouters[action.routeName];
let route; let route;
@ -290,17 +305,6 @@ export default (routeConfigs, stackConfig = {}) => {
}; };
} }
if (
action.type === NavigationActions.COMPLETE_TRANSITION &&
(action.key == null || action.key === state.key) &&
state.isTransitioning
) {
return {
...state,
isTransitioning: false,
};
}
// Handle navigation to other child routers that are not yet pushed // Handle navigation to other child routers that are not yet pushed
if (behavesLikePushAction(action)) { if (behavesLikePushAction(action)) {
const childRouterNames = Object.keys(childRouters); const childRouterNames = Object.keys(childRouters);

View File

@ -493,6 +493,23 @@ describe('StackRouter', () => {
expect(poppedImmediatelyState.isTransitioning).toBe(false); expect(poppedImmediatelyState.isTransitioning).toBe(false);
}); });
test('Navigate does not happen while transitioning', () => {
const TestRouter = StackRouter({
foo: { screen: () => <div /> },
bar: { screen: () => <div /> },
});
const initState = {
...TestRouter.getStateForAction(NavigationActions.init()),
isTransitioning: true,
};
const pushedState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'bar' }),
initState
);
expect(pushedState.index).toEqual(0);
expect(pushedState.routes.length).toEqual(1);
});
test('Navigate Pushes duplicate routeName', () => { test('Navigate Pushes duplicate routeName', () => {
const TestRouter = StackRouter({ const TestRouter = StackRouter({
foo: { screen: () => <div /> }, foo: { screen: () => <div /> },
@ -500,13 +517,13 @@ describe('StackRouter', () => {
}); });
const initState = TestRouter.getStateForAction(NavigationActions.init()); const initState = TestRouter.getStateForAction(NavigationActions.init());
const pushedState = TestRouter.getStateForAction( const pushedState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'bar' }), NavigationActions.navigate({ routeName: 'bar', immediate: true }),
initState initState
); );
expect(pushedState.index).toEqual(1); expect(pushedState.index).toEqual(1);
expect(pushedState.routes[1].routeName).toEqual('bar'); expect(pushedState.routes[1].routeName).toEqual('bar');
const pushedTwiceState = TestRouter.getStateForAction( const pushedTwiceState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'bar' }), NavigationActions.navigate({ routeName: 'bar', immediate: true }),
pushedState pushedState
); );
expect(pushedTwiceState.index).toEqual(2); expect(pushedTwiceState.index).toEqual(2);
@ -540,7 +557,7 @@ describe('StackRouter', () => {
}); });
const initState = TestRouter.getStateForAction(NavigationActions.init()); const initState = TestRouter.getStateForAction(NavigationActions.init());
const pushedState = TestRouter.getStateForAction( const pushedState = TestRouter.getStateForAction(
NavigationActions.push({ routeName: 'bar' }), NavigationActions.push({ routeName: 'bar', immediate: true }),
initState initState
); );
expect(pushedState.index).toEqual(1); expect(pushedState.index).toEqual(1);