DEV Community

Cover image for React Native Navigation and its caveats
Richard Rosko for Sudo Labs

Posted on

React Native Navigation and its caveats

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 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:

stack: {
  children: [{
    component: {
      name: 'screen.Home'
    }
  }]
}
Enter fullscreen mode Exit fullscreen mode

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

Navigation.registerComponentWithRedux()
Enter fullscreen mode Exit fullscreen mode

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:

Navigation.registerComponentWithRedux('screen.Home', () => Root, Provider, store)
Enter fullscreen mode Exit fullscreen mode

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:

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:

const navigateToSelectScreen = ({ rootComponentId }) => {
  Navigate.push(rootComponentId, {
    component: 'screen.ListSelect'
  })
}

const ListSelect = ({ rootComponentId }) => (
  <View>
    <TouchableOpacity onPress={() => { navigateToSelectScreen({ rootComponentId }) }}>
      <Text>
        Select a value
      </Text>
    </TouchableOpacity>
  </View>
)
Enter fullscreen mode Exit fullscreen mode

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:

Navigation.registerComponentWithRedux('screen.ListSelect', () => ListSelect, Provider, store)
Enter fullscreen mode Exit fullscreen mode

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:

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()
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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

  Navigation.events().registerComponentDidAppearListener()
Enter fullscreen mode Exit fullscreen mode

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.

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 }
Enter fullscreen mode Exit fullscreen mode

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:

const Provider = ({ children, store }) => (
  <ReduxProvider store={store}>
    <ThemeProvider theme={theme}>
      <AnyOtherProviderThatYouMightBeUsing>
        <NavigationComponentIdProvider>
          {children}
        </NavigationComponentIdProvider>
      </AnyOtherProviderThatYouMightBeUsing>
    </ThemeProvider>
  </ReduxProvider>
)
Enter fullscreen mode Exit fullscreen mode

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:

import { NavigationComponentIdConsumer } from './NavigationComponentIdContext'

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

export default withNavigationComponentIdContext
Enter fullscreen mode Exit fullscreen mode

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

ListSelect.js
...

export default withNavigationComponentIdContext(ListSelect)
Enter fullscreen mode Exit fullscreen mode

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 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

Top comments (0)