New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature Request] Can stack router have `replace` like History API? #802

Closed
joonhocho opened this Issue Mar 24, 2017 · 32 comments

Comments

Projects
None yet
@joonhocho

joonhocho commented Mar 24, 2017

I think replace is very common routing pattern that html history api also has it.

You don't always want to push to stack, but sometimes want to replace it with or without animation instead of going through pop and push (two animation phases).

According to doc, calling reset will empty your stack, but that's not how replace should work.

It should work like so:
state: a -> b -> c
action: c calls replace(d)
result: a -> b -> d

state: a -> b -> c
action: b calls replace(d)
result: a -> d

@joonhocho

This comment has been minimized.

Show comment
Hide comment

Related #295 #774

@joonhocho

This comment has been minimized.

Show comment
Hide comment
@joonhocho

joonhocho Mar 24, 2017

Issues with using reset to replace:
#395 #691
#199
#335

Issues with using reset to replace:
#395 #691
#199
#335

@dioxide

This comment has been minimized.

Show comment
Hide comment
@dioxide

dioxide Mar 24, 2017

+1 , agree with you, the REPLACE pattern is very useful!

dioxide commented Mar 24, 2017

+1 , agree with you, the REPLACE pattern is very useful!

@justbettereveryday

This comment has been minimized.

Show comment
Hide comment
@justbettereveryday

justbettereveryday Mar 25, 2017

I just think that : it will empty the stack,but all objects you have registered before are also here,so you can use them as usual.

I just think that : it will empty the stack,but all objects you have registered before are also here,so you can use them as usual.

@joonhocho

This comment has been minimized.

Show comment
Hide comment
@joonhocho

joonhocho Mar 25, 2017

@justbettereveryday Could you explain more about how to get all objects you have registered?

@justbettereveryday Could you explain more about how to get all objects you have registered?

@maximovicd

This comment has been minimized.

Show comment
Hide comment
@maximovicd

maximovicd Mar 25, 2017

I also use replace in my app and I think it's a good idea to have an action like that.

Until that gets done, I have used a workaround of passing my own flag into a navigate action, and when I detect it, I manually kick the last route from state before doing getStateForAction. It seems to work, with a little problem of missing the animation.

I also use replace in my app and I think it's a good idea to have an action like that.

Until that gets done, I have used a workaround of passing my own flag into a navigate action, and when I detect it, I manually kick the last route from state before doing getStateForAction. It seems to work, with a little problem of missing the animation.

@justbettereveryday

This comment has been minimized.

Show comment
Hide comment
@justbettereveryday

justbettereveryday Mar 29, 2017

@joonhocho
Just like this:

A.js

  const DefaultLaunch = StackNavigator({

    Main: { screen: Main },

    Login: { screen: LoginComponent },

    MySetting: { screen: MySetting }},
   {
        headerMode: 'screen'
    })

Main.js

const MainScreen = TabNavigator({
    Task: { screen: Task },
    Shop: { screen: Shop }
})

then you can reset your current vc anytime!

justbettereveryday commented Mar 29, 2017

@joonhocho
Just like this:

A.js

  const DefaultLaunch = StackNavigator({

    Main: { screen: Main },

    Login: { screen: LoginComponent },

    MySetting: { screen: MySetting }},
   {
        headerMode: 'screen'
    })

Main.js

const MainScreen = TabNavigator({
    Task: { screen: Task },
    Shop: { screen: Shop }
})

then you can reset your current vc anytime!

@aranda-adapptor aranda-adapptor referenced this issue Apr 6, 2017

Closed

Road to v1 stable #723

3 of 9 tasks complete
@ericvicenti

This comment has been minimized.

Show comment
Hide comment
@ericvicenti

ericvicenti Apr 13, 2017

Contributor

I think it is a good idea to have replace functionality.

What if we added a key to the navigate action, so that you could specify the current key to replace the current screen?

Lets track this issue as a part of #135, where we plan on adding a key to the navigate action

Contributor

ericvicenti commented Apr 13, 2017

I think it is a good idea to have replace functionality.

What if we added a key to the navigate action, so that you could specify the current key to replace the current screen?

Lets track this issue as a part of #135, where we plan on adding a key to the navigate action

@spencercarli

This comment has been minimized.

Show comment
Hide comment
@spencercarli

spencercarli Apr 24, 2017

Member

For anyone else that comes along this issue I'm currently using the following solution. I specify a new action type inside the getStateForAction handler and then use dispatch rather than navigate in my component.

const prevGetStateForActionHomeStack = HomeStack.router.getStateForAction;
HomeStack.router = {
  ...HomeStack.router,
  getStateForAction(action, state) {
    if (state && action.type === 'ReplaceCurrentScreen') {
      const routes = state.routes.slice(0, state.routes.length - 1);
      routes.push(action);
      return {
        ...state,
        routes,
        index: routes.length - 1,
      };
    }
    return prevGetStateForActionHomeStack(action, state);
  },
};
replaceScreen = () => {
  const { locations, position } = this.props.navigation.state.params;
  this.props.navigation.dispatch({
    key: 'NearMeMap',
    type: 'ReplaceCurrentScreen',
    routeName: 'NearMeMap',
    params: { locations, position },
  });
};

I also put together a short video walking through this code, if that would help anyone.

Member

spencercarli commented Apr 24, 2017

For anyone else that comes along this issue I'm currently using the following solution. I specify a new action type inside the getStateForAction handler and then use dispatch rather than navigate in my component.

const prevGetStateForActionHomeStack = HomeStack.router.getStateForAction;
HomeStack.router = {
  ...HomeStack.router,
  getStateForAction(action, state) {
    if (state && action.type === 'ReplaceCurrentScreen') {
      const routes = state.routes.slice(0, state.routes.length - 1);
      routes.push(action);
      return {
        ...state,
        routes,
        index: routes.length - 1,
      };
    }
    return prevGetStateForActionHomeStack(action, state);
  },
};
replaceScreen = () => {
  const { locations, position } = this.props.navigation.state.params;
  this.props.navigation.dispatch({
    key: 'NearMeMap',
    type: 'ReplaceCurrentScreen',
    routeName: 'NearMeMap',
    params: { locations, position },
  });
};

I also put together a short video walking through this code, if that would help anyone.

@joonhocho

This comment has been minimized.

Show comment
Hide comment
@joonhocho

joonhocho Apr 24, 2017

@spencercarli Awesome! Do you know how to animate replace action by any chance?

@spencercarli Awesome! Do you know how to animate replace action by any chance?

@spencercarli

This comment has been minimized.

Show comment
Hide comment
@spencercarli

spencercarli Apr 25, 2017

Member

@joonhocho I don't - do you have an example of what kind of animation you're thinking of?

Member

spencercarli commented Apr 25, 2017

@joonhocho I don't - do you have an example of what kind of animation you're thinking of?

@wilomgfx

This comment has been minimized.

Show comment
Hide comment
@wilomgfx

wilomgfx Apr 26, 2017

@spencercarli probably the base animation it has when navigating to a screen :p

@spencercarli probably the base animation it has when navigating to a screen :p

@timscott

This comment has been minimized.

Show comment
Hide comment
@timscott

timscott Jun 3, 2017

@spencercarli - I tried your solution, and it's not woking for me.

I check MyNavigator.router.getStateForAction just before I render it (<MyNavigator />), and it points to my custom function. However, when I reach the point to replace a route and call this.props.navigation.dispatch I can see that it's calling the original getStateForAction.

Thoughts?

timscott commented Jun 3, 2017

@spencercarli - I tried your solution, and it's not woking for me.

I check MyNavigator.router.getStateForAction just before I render it (<MyNavigator />), and it points to my custom function. However, when I reach the point to replace a route and call this.props.navigation.dispatch I can see that it's calling the original getStateForAction.

Thoughts?

@cyberdelphos

This comment has been minimized.

Show comment
Hide comment
@cyberdelphos

cyberdelphos Jun 13, 2017

@timscott I had to change the code a bit and it's working correctly.
Instead of:

HomeStack.router = {
  ...HomeStack.router,
  getStateForAction(action, state) {
     ...

Modified it directly:

HomeStack.router.getStateForAction = (action, state) => {
  ... (everything else remains the same)

You can see this direct modification mentioned in the docs for routers: https://reactnavigation.org/docs/routers/

@timscott I had to change the code a bit and it's working correctly.
Instead of:

HomeStack.router = {
  ...HomeStack.router,
  getStateForAction(action, state) {
     ...

Modified it directly:

HomeStack.router.getStateForAction = (action, state) => {
  ... (everything else remains the same)

You can see this direct modification mentioned in the docs for routers: https://reactnavigation.org/docs/routers/

@bleonard

This comment has been minimized.

Show comment
Hide comment
@bleonard

bleonard Jun 25, 2017

In case it's helpful, I tried a different approach to a new action name to try and not mess around as much as possible with the normal implementation of getStateForAction.

When I navigated, I did this:

navigation.navigate("ScreenName", { replaceRoute: true });

And then looked for that in the getStateForAction. By messing with the routes the normal one returned, I felt better about being more resistant to various interesting things the built-in stack router is doing.

/* @flow */

import type {
  NavigationAction,
  NavigationState
} from "react-navigation/lib/TypeDefinition";

import NavigationActions from "react-navigation/lib/NavigationActions";

const updateStackRouter = function(StackNavigator: any) {
  // generally defer to the "real" one
  const parentGetStateForAction = StackNavigator.router.getStateForAction;
  StackNavigator.router.getStateForAction = function(
    action: NavigationAction,
    inputState?: ?NavigationState
  ): ?NavigationState {
    const state: ?NavigationState = parentGetStateForAction(action, inputState);

    // fix it up if applicable
    if (state && action.type === NavigationActions.NAVIGATE) {
      if (action.params && action.params.replaceRoute) {
        delete action.params.replaceRoute;
        if (state.routes.length > 1 && state.index > 0) {
          const oldIndex = state.index - 1;
          // remove one that we are replacing
          state.routes.splice(oldIndex, 1);
          // index now one less
          state.index = oldIndex;
        }
      }
    }

    return state;
  };
};

export default updateStackRouter;

In case it's helpful, I tried a different approach to a new action name to try and not mess around as much as possible with the normal implementation of getStateForAction.

When I navigated, I did this:

navigation.navigate("ScreenName", { replaceRoute: true });

And then looked for that in the getStateForAction. By messing with the routes the normal one returned, I felt better about being more resistant to various interesting things the built-in stack router is doing.

/* @flow */

import type {
  NavigationAction,
  NavigationState
} from "react-navigation/lib/TypeDefinition";

import NavigationActions from "react-navigation/lib/NavigationActions";

const updateStackRouter = function(StackNavigator: any) {
  // generally defer to the "real" one
  const parentGetStateForAction = StackNavigator.router.getStateForAction;
  StackNavigator.router.getStateForAction = function(
    action: NavigationAction,
    inputState?: ?NavigationState
  ): ?NavigationState {
    const state: ?NavigationState = parentGetStateForAction(action, inputState);

    // fix it up if applicable
    if (state && action.type === NavigationActions.NAVIGATE) {
      if (action.params && action.params.replaceRoute) {
        delete action.params.replaceRoute;
        if (state.routes.length > 1 && state.index > 0) {
          const oldIndex = state.index - 1;
          // remove one that we are replacing
          state.routes.splice(oldIndex, 1);
          // index now one less
          state.index = oldIndex;
        }
      }
    }

    return state;
  };
};

export default updateStackRouter;
@digitalmaster

This comment has been minimized.

Show comment
Hide comment
@digitalmaster

digitalmaster Jun 30, 2017

Any movement here?.. just trying to do the same thing i did in ex-navigation like so: this.props.navigator.replace(Router.getRoute('routeName'))

Any movement here?.. just trying to do the same thing i did in ex-navigation like so: this.props.navigator.replace(Router.getRoute('routeName'))

@joshuapinter

This comment has been minimized.

Show comment
Hide comment
@joshuapinter

joshuapinter Jul 31, 2017

@cyberdelphos' corrected method works great with the latest (beta.11) version of the library.

Thanks for posting that update here @cyberdelphos! 🙏 You saved me hours trying to figure out what's wrong with @spencercarli's original method.

@cyberdelphos' corrected method works great with the latest (beta.11) version of the library.

Thanks for posting that update here @cyberdelphos! 🙏 You saved me hours trying to figure out what's wrong with @spencercarli's original method.

@leesolway

This comment has been minimized.

Show comment
Hide comment
@leesolway

leesolway Sep 1, 2017

I really liked @bleonard approach to the problem. I would love to see more helpers using this method, or having this level of simplicity such as reset.

leesolway commented Sep 1, 2017

I really liked @bleonard approach to the problem. I would love to see more helpers using this method, or having this level of simplicity such as reset.

@ahanriat

This comment has been minimized.

Show comment
Hide comment
@ahanriat

ahanriat Sep 6, 2017

Not sure if that's gonna work with nested navigators though

ahanriat commented Sep 6, 2017

Not sure if that's gonna work with nested navigators though

@bleonard

This comment has been minimized.

Show comment
Hide comment
@bleonard

bleonard Sep 10, 2017

I haven't tried with nested, but it's only modifying the one stack that it's dealing with, so I would think it was ok.

I haven't tried with nested, but it's only modifying the one stack that it's dealing with, so I would think it was ok.

@DanielRamosAcosta

This comment has been minimized.

Show comment
Hide comment
@DanielRamosAcosta

DanielRamosAcosta Sep 13, 2017

Both @spencercarli and @bleonard solutions loses the transition animation, is there any way to preserve it? Thanks

Both @spencercarli and @bleonard solutions loses the transition animation, is there any way to preserve it? Thanks

@tommeier

This comment has been minimized.

Show comment
Hide comment
@tommeier

tommeier Oct 20, 2017

Doesn't work with nested routes unfortunately

Doesn't work with nested routes unfortunately

@jinxac

This comment has been minimized.

Show comment
Hide comment
@jinxac

jinxac Nov 3, 2017

Contributor

@spencercarli thanks for sharing the code. Can you please help to understand how this would work with nested navigators? I am getting #1127 when I try this with nested routes

Contributor

jinxac commented Nov 3, 2017

@spencercarli thanks for sharing the code. Can you please help to understand how this would work with nested navigators? I am getting #1127 when I try this with nested routes

@jinxac

This comment has been minimized.

Show comment
Hide comment
@jinxac

jinxac Nov 4, 2017

Contributor

I was exploring this out a bit more and here are few things which one should be aware before implementing:-

  1. This does not work with nested routes.
  2. This does not work with tab routes.
  3. If you are trying to replace more than once screen using the code shared above you are liking to get an unidentified error message for key being missing.

To avoid point 3 I had changed my code to following:-

const defaultGetStateForAction = MainNavigation.router.getStateForAction;

MainNavigation.router.getStateForAction = (action, state) => {
  if (state && action.type === "replace") {
    const routes = state.routes.slice(0, state.routes.length - 1);
    action.key =  `id-${Date.now()}-${routes.length - 1}`;
    routes.push(action);
    return {
      ...state,
      routes,
      index: routes.length - 1
    };
  }
  return defaultGetStateForAction(action, state);
};
Contributor

jinxac commented Nov 4, 2017

I was exploring this out a bit more and here are few things which one should be aware before implementing:-

  1. This does not work with nested routes.
  2. This does not work with tab routes.
  3. If you are trying to replace more than once screen using the code shared above you are liking to get an unidentified error message for key being missing.

To avoid point 3 I had changed my code to following:-

const defaultGetStateForAction = MainNavigation.router.getStateForAction;

MainNavigation.router.getStateForAction = (action, state) => {
  if (state && action.type === "replace") {
    const routes = state.routes.slice(0, state.routes.length - 1);
    action.key =  `id-${Date.now()}-${routes.length - 1}`;
    routes.push(action);
    return {
      ...state,
      routes,
      index: routes.length - 1
    };
  }
  return defaultGetStateForAction(action, state);
};
@gianpieroc

This comment has been minimized.

Show comment
Hide comment
@gianpieroc

gianpieroc Dec 21, 2017

@jinxac what about nested routes? Any solution?

@jinxac what about nested routes? Any solution?

@jinxac

This comment has been minimized.

Show comment
Hide comment
@jinxac

jinxac Dec 22, 2017

Contributor

@gianpieroc i had used nested-routes when i started the porting of code to react-navigation but after seeing the open issues related to nesting i completing avoided it and created a single stack navigator itself. So currently no solution for nested routes either.

Contributor

jinxac commented Dec 22, 2017

@gianpieroc i had used nested-routes when i started the porting of code to react-navigation but after seeing the open issues related to nesting i completing avoided it and created a single stack navigator itself. So currently no solution for nested routes either.

@parlir

This comment has been minimized.

Show comment
Hide comment
@parlir

parlir Mar 8, 2018

For those who have nested do the following (note pullat is a lodash function but basically you just need to remove second to last element)
@gianpieroc
@jinxac

const prevGetStateForActionMainStack = MainStack.router.getStateForAction;
MainStack.router.getStateForAction = (action, state) => {
  if (state && action.type === "ReplaceCurrentScreen") {
    action.type = "Navigation/NAVIGATE"; // change it to navigate to get the next state
    let newState = prevGetStateForActionMainStack(action, state); // Let default run because nesting hard to build
    pullAt(newState.routes, [newState.routes.length - 2]); // pull out second to last element
    newState.index = newState.index - 1;
    return newState;
  }
  return prevGetStateForActionMainStack(action, state);
};

parlir commented Mar 8, 2018

For those who have nested do the following (note pullat is a lodash function but basically you just need to remove second to last element)
@gianpieroc
@jinxac

const prevGetStateForActionMainStack = MainStack.router.getStateForAction;
MainStack.router.getStateForAction = (action, state) => {
  if (state && action.type === "ReplaceCurrentScreen") {
    action.type = "Navigation/NAVIGATE"; // change it to navigate to get the next state
    let newState = prevGetStateForActionMainStack(action, state); // Let default run because nesting hard to build
    pullAt(newState.routes, [newState.routes.length - 2]); // pull out second to last element
    newState.index = newState.index - 1;
    return newState;
  }
  return prevGetStateForActionMainStack(action, state);
};
@pacozaa

This comment has been minimized.

Show comment
Hide comment
@pacozaa

pacozaa Apr 2, 2018

@brentvatne still no example in there :(

pacozaa commented Apr 2, 2018

@brentvatne still no example in there :(

@ericvicenti

This comment has been minimized.

Show comment
Hide comment
@ericvicenti

ericvicenti Apr 2, 2018

Contributor

@pacozaa, feel free to add a link to the navigation playground code in this repository which demonstrates the replace functionality, and send a pull request for that 🙂

Contributor

ericvicenti commented Apr 2, 2018

@pacozaa, feel free to add a link to the navigation playground code in this repository which demonstrates the replace functionality, and send a pull request for that 🙂

@wv1124

This comment has been minimized.

Show comment
Hide comment
@wv1124

wv1124 Apr 3, 2018

ExceptionsManager.js:63 navigation.state.routes[2].key "scene_OrderList" conflicts with another route!

wv1124 commented Apr 3, 2018

ExceptionsManager.js:63 navigation.state.routes[2].key "scene_OrderList" conflicts with another route!

@assafb81

This comment has been minimized.

Show comment
Hide comment
@assafb81

assafb81 Apr 11, 2018

Can replace() method be used to replace stacks, instead of using reset()?
currently, to replace stacks I am doing something like this:
import { NavigationActions } from 'react-navigation';

export default(navigation, stackName, screenName, params = {}) => {
    const resetAction = NavigationActions.reset({
        index: 0,
        key: null,
        actions: [
            NavigationActions.navigate({ routeName: stackName }),
        ],
    });
    navigation.dispatch(resetAction);
    if (screenName) {
        navigation.navigate(screenName, params);
    }
};

Can replace() method be used to replace stacks, instead of using reset()?
currently, to replace stacks I am doing something like this:
import { NavigationActions } from 'react-navigation';

export default(navigation, stackName, screenName, params = {}) => {
    const resetAction = NavigationActions.reset({
        index: 0,
        key: null,
        actions: [
            NavigationActions.navigate({ routeName: stackName }),
        ],
    });
    navigation.dispatch(resetAction);
    if (screenName) {
        navigation.navigate(screenName, params);
    }
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment