navigation-rfc icon indicating copy to clipboard operation
navigation-rfc copied to clipboard

Nested StackReducers and BackAction

Open domchristie opened this issue 8 years ago • 6 comments

I am using nested StackReducers to achieve the following navigation state (one for the 'main' stack, the other for the '_conversations' stack):

{
  key: 'main',
  index: 1,
  children: [
    { key: 'dashboard' },
    {
      key: '_conversations',
      index: 1,
      children: [
        { key: 'conversations' },
        { key: 'conversation' }
      ]
    }
  ]
}

I’d expect that dispatching { type: 'BackAction' } would pop the inner-most active state:

{
  key: 'main',
  index: 1,
  children: [
    { key: 'dashboard' },
    {
      key: '_conversations',
      index: 0,
      children: [
        { key: 'conversations' }
      ]
    }
  ]
}

But instead it affects both stacks resulting in:

{
  key: 'main',
  index: 0,
  children: [
    { key: 'dashboard' }
  ]
}

Is this the correct behaviour? If so, are there any techniques to prevent the action from being handled by an outer stack?

Thanks!

(Related: https://github.com/facebook/react-native/issues/6963)

domchristie avatar Jun 02 '16 14:06 domchristie

@domchristie See facebook/react-native@807726b—NavigationExperimental has no intention to support hierarchical states.

I also think reducers belong to user-level code, and the ones currently shipped with NavigationExperimental should be simplified and moved to UIExplorer for reference impl. Otherwise supporting everyone's use cases is a very quick path to a very bloated codebase. Also, docs may be forthcoming.

@ericvicenti @hedgerwang What do you guys think?

🍺

jmurzy avatar Jun 02 '16 20:06 jmurzy

@jmurzy : We're going to remove all Reducers from NavigationExperimental.

tl;dr;

For navigation actions at high level, reducers from NavigationReducers does not know anything about the app-specific state thus people won't use these reducers. Instead, people should build their own reducers.

There are a lot of good libraries available that help people to reducing things if that's what they really need.

At the low level, for navigation state changes that don't involve app-specific state, NavigationStateUtils should server that kind of need.

NavigationReducers serves very little benefit cause it does not know the app state, it does not know how to traverse the navigation states which can be a tree, a list or a map.

That said, we hold no interest in owning in the core navigation library.

hedgerwang avatar Jun 02 '16 21:06 hedgerwang

@hedgerwang My thoughts exactly. Thanks!

🍺

jmurzy avatar Jun 02 '16 21:06 jmurzy

@jmurzy @hedgerwang

Thanks for the clarification. I had seen https://github.com/facebook/react-native/commit/807726b, and was aware that some of the NavigationExperimental parts were going to be deprecated (e.g. containers), but I wasn’t entirely sure if that would impact reducers (I had not seen https://github.com/facebook/react-native/commit/69627bf91476274e92396370acff08fb20b8f3fc until now).

That said, we hold no interest in owning in the core navigation library.

I think it’s a bit of a shame that the project has taken this stance, since even the most basic app will usually require some form of navigation. However, having worked on creating a navigation library for the app I’m working on, I can understand the reasoning—dealing with the multitude of navigation permutations is far from straightforward!

I look forward to checking out the new approach with the new examples :)

In the mean time, would it be worth clarifying the state of navigation in the README? It seems that a significant part of the API is deprecated (whether you use Navigator or NavigationExperimental), which makes it a bit difficult to know where to start.


For what it’s worth, I handled the Nested StackReducer + BackAction issue by wrapping the stack reducer in another reducer. Something like:

const stackReducer = NavigationExperimental.Reducer.StackReducer({…})

function navigationReducer (state, action) {
  if (action.type === 'BackAction') return navigateBack(state, action)
  else return stackReducer(state, action)
}

navigateBack traverses the navigation state tree, until it reaches the inner-most active state. It then pops that state, and decrements the parent state’s index.

domchristie avatar Jun 03 '16 09:06 domchristie

@domchristie : navigate states don't necessarily need to be nested. For instance, say that you have three tabs "apple", "banana" and "orange". Each tab has its won scenes.

image

So how would you model the navigation states? One of the easy way to handle this is to use flatten navigation states which all navigation state is contained my a object map.

// First Step.
// Define what app navigation state will look like.
function createAppNavigationState(): Object {
  return  {
    // Three tabs.
    tabs: {
      index: 0,
      routes: [
        {key: 'apple'},
        {key: 'banana'},
        {key: 'orange'},
      ],
    },
    // Scenes for the `apple` tab.
    apple: {
      index: 0,
      routes: [{key: 'Apple Home'}],
    },
    // Scenes for the `banana` tab.
    banana: {
      index: 0,
      routes: [{key: 'Banana Home'}],
    },
    // Scenes for the `orange` tab.
    orange: {
      index: 0,
      routes: [{key: 'Orange Home'}],
    },
  };
}

then you can build the reducer around it.

// Next step.
// Define what app navigation state shall be updated.
function updateAppNavigationState(
  state: Object,
  action: Object,
): Object {
  let {type} = action;
  if (type === 'BackAction') {
    type = 'pop';
  }

  switch (type) {
    case 'push': {
      // Push a route into the scenes stack.
      const route: Object = action.route;
      const {tabs} = state;
      const tabKey = tabs.routes[tabs.index].key;
      const scenes = state[tabKey];
      const nextScenes = NavigationStateUtils.push(scenes, route);
      if (scenes !== nextScenes) {
        return {
          ...state,
          [tabKey]: nextScenes,
        };
      }
      break;
    }

    case 'pop': {
      // Pops a route from the scenes stack.
      const {tabs} = state;
      const tabKey = tabs.routes[tabs.index].key;
      const scenes = state[tabKey];
      const nextScenes = NavigationStateUtils.pop(scenes);
      if (scenes !== nextScenes) {
        return {
          ...state,
          [tabKey]: nextScenes,
        };
      }
      break;
    }

    case 'selectTab': {
      // Switches the tab.
      const tabKey: string = action.tabKey;
      const tabs = NavigationStateUtils.jumpTo(state.tabs, tabKey);
      if (tabs !== state.tabs) {
        return {
          ...state,
          tabs,
        };
      }
    }
  }
  return state;
}

the full snippet can be found at https://gist.github.com/hedgerwang/4c9cc24b729d0787a1558ff47f0ca439.

hedgerwang avatar Jun 03 '16 23:06 hedgerwang

@hedgerwang sorry to bring this back but I'm having trouble in modelling some nested Navigations. Basically I'd like to have some sort of Layout/Navigation like Whatsapp in which when you enter there's a Tab View and if you click on a Chat item the Tab View is replaced by other View (the Chat itself without the TabBar below it) which has it's own Navigation. Inside the Chat we could then navigate to other Views or return to the Tab View.

joaovpmamede avatar Aug 28 '16 23:08 joaovpmamede