Skip to content

Commit

Permalink
StackNavigator Replace Action (react-navigation#3440)
Browse files Browse the repository at this point in the history
* Navigation replace action

The long awaited action to replace the a route in StackNavigator

* Fix flow maybe
  • Loading branch information
ericvicenti authored and sourcecode911 committed Mar 9, 2020
1 parent f3e363d commit c5b582a
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 31 deletions.
9 changes: 2 additions & 7 deletions examples/NavigationPlayground/js/SimpleStack.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,8 @@ class MyNavScreen extends React.Component<MyNavScreenProps> {
title="Go to a photos screen"
/>
<Button
onPress={() =>
navigation.navigate('Profile', {
name: 'Dog',
headerBackImage: require('./assets/dog-back.png'),
})
}
title="Custom back button"
onPress={() => navigation.replace('Profile', { name: 'Lucy' })}
title="Replace with profile"
/>
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
Expand Down
85 changes: 62 additions & 23 deletions flow/react-navigation.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// @flow

declare module 'react-navigation' {

/**
* First, a bunch of things we would love to import but instead must
* reconstruct (mostly copy-pasted).
Expand Down Expand Up @@ -68,6 +67,8 @@ declare module 'react-navigation' {

// The action to run inside the sub-router
action?: NavigationNavigateAction,

key?: string,
|};

declare type DeprecatedNavigationNavigateAction = {|
Expand Down Expand Up @@ -130,8 +131,9 @@ declare module 'react-navigation' {
type: 'Reset',
index: number,
key?: ?string,
actions:
Array<NavigationNavigateAction | DeprecatedNavigationNavigateAction>,
actions: Array<
NavigationNavigateAction | DeprecatedNavigationNavigateAction
>,
|};

declare export type NavigationUriAction = {|
Expand All @@ -144,9 +146,37 @@ declare module 'react-navigation' {
uri: string,
|};

declare export type NavigationReplaceAction = {|
+type: 'Navigation/REPLACE',
+key: string,
+routeName: string,
+params?: NavigationParams,
+action?: NavigationNavigateAction,
|};
declare export type NavigationPopAction = {|
+type: 'Navigation/POP',
+n?: number,
+immediate?: boolean,
|};
declare export type NavigationPopToTopAction = {|
+type: 'Navigation/POP_TO_TOP',
+immediate?: boolean,
|};
declare export type NavigationPushAction = {|
+type: 'Navigation/PUSH',
+routeName: string,
+params?: NavigationParams,
+action?: NavigationNavigateAction,
+key?: string,
|};

declare export type NavigationAction =
| NavigationInitAction
| NavigationNavigateAction
| NavigationReplaceAction
| NavigationPopAction
| NavigationPopToTopAction
| NavigationPushAction
| NavigationBackAction
| NavigationSetParamsAction
| NavigationResetAction;
Expand Down Expand Up @@ -209,9 +239,8 @@ declare module 'react-navigation' {
params?: NavigationParams,
};

declare export type NavigationStateRoute =
& NavigationLeafRoute
& NavigationState;
declare export type NavigationStateRoute = NavigationLeafRoute &
NavigationState;

/**
* Router
Expand Down Expand Up @@ -291,12 +320,8 @@ declare module 'react-navigation' {
Route: NavigationRoute,
Options: {},
Props: {}
> =
& React$ComponentType<NavigationNavigatorProps<Options, Route> & Props>
& (
| {}
| { navigationOptions: NavigationScreenConfig<Options> }
);
> = React$ComponentType<NavigationNavigatorProps<Options, Route> & Props> &
({} | { navigationOptions: NavigationScreenConfig<Options> });

declare export type NavigationNavigator<
State: NavigationState,
Expand Down Expand Up @@ -334,16 +359,18 @@ declare module 'react-navigation' {

declare export type HeaderMode = 'float' | 'screen' | 'none';

declare export type HeaderProps = $Shape<NavigationSceneRendererProps & {
mode: HeaderMode,
router: NavigationRouter<NavigationState, NavigationStackScreenOptions>,
getScreenDetails: NavigationScene => NavigationScreenDetails<
NavigationStackScreenOptions
>,
leftInterpolator: (props: NavigationSceneRendererProps) => {},
titleInterpolator: (props: NavigationSceneRendererProps) => {},
rightInterpolator: (props: NavigationSceneRendererProps) => {},
}>;
declare export type HeaderProps = $Shape<
NavigationSceneRendererProps & {
mode: HeaderMode,
router: NavigationRouter<NavigationState, NavigationStackScreenOptions>,
getScreenDetails: NavigationScene => NavigationScreenDetails<
NavigationStackScreenOptions
>,
leftInterpolator: (props: NavigationSceneRendererProps) => {},
titleInterpolator: (props: NavigationSceneRendererProps) => {},
rightInterpolator: (props: NavigationSceneRendererProps) => {},
}
>;

/**
* Stack Navigator
Expand Down Expand Up @@ -493,6 +520,18 @@ declare module 'react-navigation' {
eventName: string,
callback: NavigationEventCallback
) => NavigationEventSubscription,
push: (
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction
) => boolean,
replace: (
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction
) => boolean,
pop: (n?: number, params?: { immediate?: boolean }) => boolean,
popToTop: (params?: { immediate?: boolean }) => boolean,
};

declare export type NavigationNavigatorProps<O: {}, S: {}> = $Shape<{
Expand Down Expand Up @@ -767,7 +806,7 @@ declare module 'react-navigation' {
>(
router: NavigationRouter<S, O>,
routeConfigs?: NavigationRouteConfigMap,
navigatorConfig?: NavigatorConfig,
navigatorConfig?: NavigatorConfig
): _NavigatorCreator<NavigationViewProps, S, O>;

declare export function StackNavigator(
Expand Down
12 changes: 12 additions & 0 deletions src/NavigationActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const POP = 'Navigation/POP';
const POP_TO_TOP = 'Navigation/POP_TO_TOP';
const PUSH = 'Navigation/PUSH';
const RESET = 'Navigation/RESET';
const REPLACE = 'Navigation/REPLACE';
const SET_PARAMS = 'Navigation/SET_PARAMS';
const URI = 'Navigation/URI';
const COMPLETE_TRANSITION = 'Navigation/COMPLETE_TRANSITION';
Expand Down Expand Up @@ -79,6 +80,15 @@ const reset = createAction(RESET, payload => ({
actions: payload.actions,
}));

const replace = createAction(REPLACE, payload => ({
type: REPLACE,
key: payload.key,
params: payload.params,
action: payload.action,
routeName: payload.routeName,
immediate: payload.immediate,
}));

const setParams = createAction(SET_PARAMS, payload => ({
type: SET_PARAMS,
key: payload.key,
Expand Down Expand Up @@ -157,6 +167,7 @@ export default {
POP_TO_TOP,
PUSH,
RESET,
REPLACE,
SET_PARAMS,
URI,
COMPLETE_TRANSITION,
Expand All @@ -169,6 +180,7 @@ export default {
popToTop,
push,
reset,
replace,
setParams,
uri,
completeTransition,
Expand Down
10 changes: 10 additions & 0 deletions src/addNavigationHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,15 @@ export default function(navigation) {
navigation.dispatch(
NavigationActions.push({ routeName, params, action })
),

replace: (routeName, params, action) =>
navigation.dispatch(
NavigationActions.replace({
routeName,
params,
action,
key: navigation.state.key,
})
),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
"pop": [Function],
"popToTop": [Function],
"push": [Function],
"replace": [Function],
"setParams": [Function],
"state": Object {
"index": 0,
Expand Down Expand Up @@ -337,6 +338,7 @@ exports[`StackNavigator renders successfully 1`] = `
"pop": [Function],
"popToTop": [Function],
"push": [Function],
"replace": [Function],
"setParams": [Function],
"state": Object {
"index": 0,
Expand Down
28 changes: 27 additions & 1 deletion src/routers/StackRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export default (routeConfigs, stackConfig = {}) => {
}
}

//Handle pop-to-top behavior. Make sure this happens after children have had a chance to handle the action, so that the inner stack pops to top first.
// Handle pop-to-top behavior. Make sure this happens after children have had a chance to handle the action, so that the inner stack pops to top first.
if (action.type === NavigationActions.POP_TO_TOP) {
if (state.index !== 0) {
return {
Expand All @@ -183,6 +183,31 @@ export default (routeConfigs, stackConfig = {}) => {
return state;
}

// Handle replace action
if (action.type === NavigationActions.REPLACE) {
const routeIndex = state.routes.findIndex(r => r.key === action.key);
// Only replace if the key matches one of our routes
if (routeIndex !== -1) {
const childRouter = childRouters[action.routeName];
let childState = {};
if (childRouter) {
const childAction =
action.action ||
NavigationActions.init({ params: action.params });
childState = childRouter.getStateForAction(childAction);
}
const routes = [...state.routes];
routes[routeIndex] = {
params: action.params,
// merge the child state in this order to allow params override
...childState,
key: action.key,
routeName: action.routeName,
};
return { ...state, routes };
}
}

// Handle explicit push navigation action. Make sure this happens after children have had a chance to handle the action
if (
behavesLikePushAction(action) &&
Expand Down Expand Up @@ -235,6 +260,7 @@ export default (routeConfigs, stackConfig = {}) => {
action.action || NavigationActions.init({ params: action.params });
route = {
params: action.params,
// merge the child state in this order to allow params override
...childRouter.getStateForAction(childAction),
key,
routeName: action.routeName,
Expand Down
22 changes: 22 additions & 0 deletions src/routers/__tests__/StackRouter-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,28 @@ describe('StackRouter', () => {
});
});

test('Replace action works', () => {
const TestRouter = StackRouter({
foo: { screen: () => <div /> },
bar: { screen: () => <div /> },
});
const initState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'foo' })
);
const replacedState = TestRouter.getStateForAction(
NavigationActions.replace({
routeName: 'bar',
params: { meaning: 42 },
key: initState.routes[0].key,
}),
initState
);
expect(replacedState.index).toEqual(0);
expect(replacedState.routes.length).toEqual(1);
expect(replacedState.routes[0].routeName).toEqual('bar');
expect(replacedState.routes[0].params.meaning).toEqual(42);
});

test('Handles push transition logic with completion action', () => {
const FooScreen = () => <div />;
const BarScreen = () => <div />;
Expand Down
1 change: 1 addition & 0 deletions src/views/__tests__/__snapshots__/TabView-test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ exports[`TabBarBottom renders successfully 1`] = `
"pop": [Function],
"popToTop": [Function],
"push": [Function],
"replace": [Function],
"setParams": [Function],
"state": Object {
"key": "s1",
Expand Down

0 comments on commit c5b582a

Please sign in to comment.