Structuring React Native app navigation

Lift off

By default, react-native doesn’t come with any navigation solution out of the box. Even though there is a recommended one —react-navigation, there are a bunch of other possible ways to answer the “How to navigate?” question.

Most of them are made in pure JS, which is not bad, but if your desire is to have truly native navigation you’ve got used to in either Android or iOS, you’ll probably end up with the react-native-navigation (RNN for the sake of simplicity) by Wix.
This is a great library, however, there is a problem… 🤔

The problem of navigation

Apart from quiet a complex installation process, RNN has quite a cumbersome API for describing screen’s layouts. The screen’s layout should be described via deeply nested JS objects and arrays, which is fairly natural in the JS world, yet it’s quite hard to visually “parse” it in code.

App navigation structure explained

The thing is that when either pushing a new screen or showing a modal one, you ought to pass an object with an almost complete screen’s layout description, which could become huge for a complex screen especially with additional styling options.

The solution

But fear not! Due to the fact that everything is described by the object, we can easily extract certain parts of the layouts in order to reuse them further.

Let’s go straight to the solution I came up with. Here is the overall structure of the navigation module:

src/navigation
├── index.js
├── common.js
├── parts.js
├── views.js
└── screens.js

Let’s see what’s in there.

common.js

In here we will store all the common navigation actions which could be reused across the app. Those could be tab switching logic, logic for redirecting the user to the certain place in the app in case of login out and so on and so forth.

const switchToTab = (componentId, index) => {
  Navigation.mergeOptions(componentId, {
    bottomTabs: {
      currentTabIndex: index,
    },
  })
  Navigation.popToRoot(componentId)
}

parts.js

That’s the place where small parts of the screens’ layouts belong to. Here we can store bottom tabs, buttons, header components or a side menu toggle button (a.k.a “hamburger”) as in the example below:

const buttonHamburger = componentId => ({
  id: 'button.Hamburger',
  component: {
    name: 'component.Hamburger',
    passProps: {
      onPress: isOpened => Navigation.mergeOptions(componentId, {
        sideMenu: {
          left: {
            visible: isOpened,
          },
        },
      }),
    },
  },
})

views.js

And here is the place for our layout’s components. They are almost ready screens’ layouts and could later be placed in different containers: a stack, a bottom or top tabs or side menus.

import parts from './parts'

...

const news = () => ({
  component: {
    id: 'screen.NewsScreen',
    name: 'screen.NewsScreen',
    options: {
      bottomTab: parts.tab('news'),
      sideMenu: parts.sideMenuLeftEnabled(true),
      topBar: {
        title: {
          component: parts.headerWithText({
            label: 'News',
          }),
        },
        leftButtons: [
          parts.buttonHamburger('screen.NewsScreen'),
        ],
      },
    },
  },
})

screens.js

And the screens finally. The containers we’ve been talking earlier. Here we combine all the things together to get the final screen layout to be fed to the RNN.

Let’s see how the layout for the app with a side menu and bottom tabs could look:

import parts from './parts'
import views from './views'

...

const current = () => ({
  stack: {
    id: 'tab.CurrentScreen',
    children: [
      views.current(),
    ],
  },
})

const forecase = () => ({
  stack: {
    id: 'tab.ForecastScreen',
    children: [
      views.forecase(),
    ],
  },
})

const map = () => ({
  stack: {
    id: 'tab.MapScreen',
    children: [
      views.map(),
    ],
  },
})

const main = () => ({
  root: {
    sideMenu: {
      left: parts.sideMenuLeft(),
      center: {
        bottomTabs: {
          id: 'tabs.Bottom',
          children: [
            current(),
            forecash(),
            map(),
          ],
        },
      },
    },
  },
})

Nice and clean navigation

And finally, after all these manipulations we can perform the navigation in a nice and clean way.

Setting the root layout for the whole app would look just like that:

import { Navigation } from 'react-native-navigation'
import navigation from 'src/navigation'

...

Navigation.setRoot(navigation.screens.main())

...

and navigating to a new screen:

import { Navigation } from 'react-native-navigation'
import navigation from 'src/navigation'

...

Navigation.push(this.props.componentId, navigation.screens.forecast())

...

With such an approach we have a single source of truth for all the navigation layouts and common actions and a better separation into small reusable components like parts, views, and screens.

Originally published on Medium by Pavel Vashkel.

Pavel Vashkel

Pavel Vashkel

Mobile developer at datarockets

From our blog

Stay up to date

Check out our newsletter