React Native Navigation and its caveats
Tags: javascript, reactnative
Published: 2019-05-14 12:48:41

So you went out on a journey to create a `react-native` application and you feel like you have superpowers. You create your components, style them, make API calls just like you do on the web and you're practically walking on sunshine.

But then you want to implement navigation and the world starts crumbling around you. You try out the navigation that comes pre-packaged in React Native, but it's not cross-platform consistent. Then you discover a JavaScript solution, but the performance is sub-optimal for most apps. You need a solution that uses native navigation definitions. And then you discover `react-native-navigation` (RNN from now on) from Wix and you are saved as it has all you need.

It is a native solution, that boasts great performance, uses native events on the background and is cross-platform in its true meaning.

Unfortunately, it has some weird issues and you are once again lost. Do not give up hope yet though! Rather than that, let's walk through the breaking points and jot down some notes on how to work with/around those weird issues.

## `v1` VS `v2`

The guys over at Wix are working really hard on providing all of us with a native cross-platform solution to navigate in React Native apps - huge kudos to them for maintaining the library. With that being said, the documentation is a bit rough around the edges and sometimes it's hard to find what you are looking for.

A quick look into the the ecosystem of RNN will reveal, that there are two versions of the library that are currently being maintained - the older `v1` and the "brand" new `v2`.

The differences between the two versions are marginal and although there is a [way](https://github.com/wix-playground/react-native-navigation-v1-v2-adapter) to migrate v1  to v2, it's best to make your mind up before starting a new project.

The question is, which one should you use for your new React Native project?

The quick answer is: `v2`.

The long answer is, that both versions are still actively maintained, but `v2` has a priority in the development process. And although there are some features, that are not yet implemented in `v2`, the team is very active in making that a thing of the past. Also, the official recommendation from Wix is to use `v2` in your projects and so, we should stick with what the authors are saying in this case.

## Stacks

This is something you will probably use in any app that uses navigation. A general consensus of what a stack actually is would probably be that is it a collection of screens that are somehow related and, therefore, can be navigated *to* and *from*. This does not, however, directly translate to how the stacks work in RNN applications.

In my opinion, it's just an issue with naming here. `stack` in RNN simply represents an arbitrary screen, that you can use to navigate to any other arbitrary screen. Don't try to find any deeper meaning in the name of the definition here.

The way you define a stack in and RNN app is this:
```jsx
stack: {
  children: [{
    component: {
      name: 'screen.Home'
    }
  }]
}
```
If the `Home` screen is registered with this definition you can use `Navigation.push()` (and all other navigation events) from the `Home` screen to **any** other registered screen. They do not have to be defined in the same `stack: {}` in order for those transition to work. This was an issue for me when I started using RNN as it makes little sense to me given the naming. I hope you learn from my mistake and navigate properly with `stack` definitions from now on.

One more word of caution here though - I have seen people who define all of their screens in `stacks` to make sure they are able to navigate from them if the need arises. I wouldn't personally do that, because if you define a screen in `stack` it naturally adds some overhead code on the native side to your screens that are defined this way and that is not optimal. You are better off paying attention to the definitions of your screens and only define the screens which you will be navigating from with `Navigation` events. Such is the will of RNN.

## Wrapping registered components in Redux (or any other Provider)

Can you use RNN in conjunction with Redux? Yes, you can. But you have to do some prep work to make it possible. You will probably find many different ways to do it if you make a quick Google search, but there is only one official way and that is through the RNN's own API. The only thing you need to do is use
```js
Navigation.registerComponentWithRedux()
```
to register your components. This function accepts exactly three parameters, the first being the component you want to register, the second being the `Provider` (`import { Provider } from react-redux`) and the third being your custom redux store. In the end, you should have something like this:
```jsx
Navigation.registerComponentWithRedux('screen.Home', () => Root, Provider, store)
```
Why stop there though? Even though the method is called `registerComponentWithRedux`, what it actually does is, it wraps all of the defined components with whatever component you provide it as the second parameter. Therefore, this is the ideal spot to wrap your component with whatever else you need to, like a `ThemeProvider`, or, e.g. React's `Contexts` if you use those in your app.

Not only is this the ideal place to wrap your app in any wrappers, but it's probably one of the only places you can do so. Why is that you might ask? Well, you probably already figured it out when you were initializing RNN in your app. The way RNN works is it pre-registers all of your screens ahead of the time for optimization purposes. This is a good thing, but it also unavoidably means, that the entry point of your app changes from being declarative (that's the way, uh huh huh, we like it, uh huh huh) to being imperative.

React likes it much more, if we use declarative definitions for our codebase (layouts and all). But if you use RNN in your app it's just not possible to do that for the root components of your app. This is my main beef with the library, but that's a topic for another time and at least there is a way to wrap your top-level components, albeit not being the most user-friendly.

### `componentId` and making peace with it using `React.Context`

As mentioned in the first article, there are two versions of the library. Let's assume that you are using the preferred `v2`. One of the main changes to the API of the library is the presence of `componentId` - a unique identifier of any RNN registered component. You have to use it anytime you want to perform a `Navigation` event (e.g. `.push()`, `.pop()`) and each root component that is registered in the initialization step of an RNN app receives it as a prop. This fact could prove problematic and here's why.

Let's say you want to create a reusable component for your app that needs to perform a navigation event as part of its functionality. You could define it as part of your route stack, but that wouldn't really make sense from a functional point of view. It is not a specific screen that the app displays, but just an arbitrary component that could be reused. This approach could bring its own fair share of problems, like unnecessary code duplication and complicated definitions in the scope of a component.

This is better demonstrated on an example. Let's create a component that acts as a select screen with many options that displays an input and when tapped, it presents a separate screen that shows all the possible values for this input in a table. One of these components, let's call it `ListSelect`:

![](https://thepracticaldev.s3.amazonaws.com/i/q8jhalqu69ecoc6xlaj5.png)

Since you need to navigate from the screen that is currently displayed when the user interacts with this component you need to have the `componentId` of the mounted screen ready for the `Navigation` event in our `ListSelect` component. If the `ListSelect` component is used directly in the screen that has a `componentId`, that wouldn't be a problem. You'd just need to pass it as a prop to the component and then inside of it you would receive it and perform the event, like this:
```jsx
const navigateToSelectScreen = ({ rootComponentId }) => {
  Navigate.push(rootComponentId, {
    component: 'screen.ListSelect'
  })
}

const ListSelect = ({ rootComponentId }) => (
  <View>
    <TouchableOpacity onPress={() => { navigateToSelectScreen({ rootComponentId }) }}>
      <Text>
        Select a value
      </Text>
    </TouchableOpacity>
  </View>
)
```
But what if you use it extensively and you are sick of doing the `<ListSelect rootComponentId={componentId} />`? Or even worse, what if the component is nested deeper in the hierarchy and you'd need to prop drill it three, or even more levels? There has to be a better solution. Enter `Provider`.

In the previous section we were talking about how we can wrap our root components with a custom provider. What we could do here, is wrap our top level components with a Context Provider that stores the `componentId` property which we can then access in a `Consumer` to use literally anywhere else in the app and down the hierarchy tree. Context is a great way to lift such a UI specific piece of data and make it accessible in the hierarchy, but you could use any other global state management library, e.g. Redux, or Flux with the same results.

Let's get back to our example and trace the steps we need to make to ensure we can use the component anywhere with no hassle. First of all, we define the screen we will be navigating to, from inside the component in our stack, like this:
```jsx
Navigation.registerComponentWithRedux('screen.ListSelect', () => ListSelect, Provider, store)
```
The next thing we need to do is create a Context that will keep track of the currently visible `componentId` for us, let's call it `NavigationComponentIdProvider` (duh). It's going to look something like this:
```jsx
class NavigationComponentIdProvider extends React.Component {
  state = { navigationComponentId: '' }
  componentDidMount() {
    this.navigationEventsListener =
      Navigation.events().registerComponentDidAppearListener(({ componentId }) => {
        this.setState({
          navigationComponentId: componentId
        })
      })
  }
  componentWillUnmount() {
    if (this.navigationEventsListener) {
      this.navigationEventsListener.remove()
    }
  }
}
```
In the `componentDidMount` we set up a listener that updates the state anytime a navigation event changes the active component. RNN has a useful listener for that using the provided
```js
  Navigation.events().registerComponentDidAppearListener()
```
method that receives the `componentId` when it is called. We then simply update the state with this information. Do not forget to remove the listener in `componentWillUnmount`.

We wrap it up by finishing the render method, which renders the `children` wrapped with the Context's Provider. This should be the end result of our newly created Context that knows the currently displayed `componentId` at any given time.
```jsx
class NavigationComponentIdProvider extends React.Component {
  state = { navigationComponentId: '' }
  componentDidMount() {
    this.navigationEventsListener =
      Navigation.events().registerComponentDidAppearListener(({ componentId }) => {
        this.setState({
          navigationComponentId: componentId
        })
      })
  }
  componentWillUnmount() {
    if (this.navigationEventsListener) {
      this.navigationEventsListener.remove()
    }
  }
  render() {
    const { children } = this.props
    const { navigationComponentId } = this.state
    return (
      <NavigationComponentIdContext.Provider value={navigationComponentId}>
        {children}
      </NavigationComponentIdContext.Provider>
    )
  }
}

const NavigationComponentIdConsumer = NavigationComponentIdContext.Consumer

export { NavigationComponentIdConsumer, NavigationComponentIdProvider }
```
The next step is to update our global `Provider` that we wrap all our top-level components with to include the `NavigationComponentIdProvider`.

Our `Provider` should look something like this:
```jsx
const Provider = ({ children, store }) => (
  <ReduxProvider store={store}>
    <ThemeProvider theme={theme}>
      <AnyOtherProviderThatYouMightBeUsing>
        <NavigationComponentIdProvider>
          {children}
        </NavigationComponentIdProvider>
      </AnyOtherProviderThatYouMightBeUsing>
    </ThemeProvider>
  </ReduxProvider>
)
```
If you have followed the tutorial up to this point, you can now `import { NavigationComponentIdConsumer }'` and get the `componentId` of a displayed screen anywhere in your app.

To make the whole process even simpler and cleaner, we can create a higher order component (HOC) that wraps the component with the `NavigationComponentIdConsumer`. It's a very simple abstraction and it will help us to keep the resulting code much cleaner.

The HOC could look something like this:
```jsx
import { NavigationComponentIdConsumer } from './NavigationComponentIdContext'

const withNavigationComponentIdContext = Comp => props => (
  <NavigationComponentIdConsumer>
    {componentId => <Comp componentId={componentId} {...props} />}
  </NavigationComponentIdConsumer>
)

export default withNavigationComponentIdContext
```
The last thing we do is wrap the export of our `ListSelect` component in `withNavigationComponentIdContext`, like so.
```jsx
ListSelect.js
...

export default withNavigationComponentIdContext(ListSelect)
```
The component now has access to the `componentId` from its props and we do not need to pass it in manually.

The great thing about this is, that you can use this HOC anywhere in your app, so you needn't worry about the `componentId` anymore if you don't want to . If you are in a situation where a component that is on a lower level in hierarchy needs to make a `Navigation` event, don't drill the `componentId` manually, just wrap it in `withNavigationComponentIdContext` and get it in the props.

Another good example for this usage would be creating a modal component. Although if modals are very specific in your app, it might make sense to define them as separate screens to make more sense from the semantical point of view.

## Takeaways

There are a few more RNN specific issues that come to mind. But most of those are version specific (RNN is updated almost every day!). It's probably a good idea to leave them be and not try to work around them that much as they are bound to be resolved in the near future. This article should serve as a quick write-up with solutions to common issues in RNN that are probably not likely to change, since they are tied to the API of the library.

Other than that, if you have any more issues, I can definitely recommend the Discord [community](https://discord.gg/hnc8nq) for `react-native-navigation`. Folks there are very helpful and saved my skin loads of times before. Thanks for reading!

---
*First published on [sudolabs.io](https://sudolabs.io/blog/react-native-navigation-and-its-caveats/)*