After reviewing a number of options, we at Eaze decided to use react-native-navigation by Wix in our application. However, we found it difficult to use their API in an app as complex as ours. In particular, we found it excessively limiting to only have access to the navigator through component props. This article describes the solution that we eventually devised and implemented.

We originally wrote our app using react-native-router-flux, a pure Javascript navigator. While it was easy to use, we ultimately decided to replace it with react-native-navigation for the following reasons:

With pure Javascript navigators, when there is a backstack of components, they are present in the view tree. This produces a significant amount of GPU overdraw, which my colleague discussed in this article. As a corollary to the above, accessibility is broken for visually-impaired individuals who are using a screen reader, as it detects all of the components in the backstack. You can read more about this in the bug report I filed here. We wanted transitions and navigation that had a truly native look and feel.

However, we found ourselves frustrated with the limitation that, with react-native-navigation, we must access the navigator via the React component props. In particular, we would often want to perform navigation in an action. For example, we might want to show an error message without necessarily knowing which screen is currently displayed. Inextricably tying navigation to UI components seemed like a poor separation of concerns. It also made it particularly difficult to port our existing codebase over from react-native-router-flux, which does not have this constraint.

As we were already using redux and thunk as part of our tech stack, we decided to create a redux wrapper for navigation functionality. In our design, we can dispatch push, reset, and pop events the same way as we would any action. We also created a higher-order component used to wrap our containers which would automatically respond to the changes in the redux store and issue the navigation change. Thus, the general flow is as follows.

An action is dispatched to the redux store, causing our reducer to update it with the new screen to which to navigate, and information as to whether this is a push, pop, or resetTo. Our parent component receives new props and checks whether or not the screen is changing. If so, it calls the navigator directly, via this.props.navigator .

Our application has a map of routes that we iterate through when calling registerComponent . Each item has a field id which is a unique string identifier for each navigable component, and a corresponding field component which is the imported React component itself.

For this functionality, we maintain the following fields in our redux store:

Note: we have to maintain our own backstack data structure as it is not currently provided by the navigation library.

Here is our actions file:

As you can see, it is a fairly simple set of functions to update the redux store with information. Here are the reducers:

Please note that if you use redux-persist, you will want to blacklist these values.

Finally, we have a component that is used as a parent of all our navigation components called EazeCompositor .

Let’s walk through this component code. The code in shouldComponentUpdate is the heart of this logic. If the next props for this component have a screen that is different from the current one, we examine the other redux store values discussed above. Then it performs the navigation action (push, pop, or resetTo) directly on this.props.navigator . In other words, we describe what the redux store should look like after the navigation is performed, and our parent component intercepts the store change and actually does the navigation.

I initially noticed one problem: if there were multiple components in the backstack, they might all respond to the navigation request. This happens consistently in iOS. We keep track of the currently-visible screen using the visibility listener API and update the component state accordingly.

renderChildren clones the child element and passes it the parent props. Since we connect to our push, pop, and resetTo actions in mapDispatchToProps , our containers will all be able to perform navigation with this.props without having to explicitly add this functionality in every component.

We put this all together by wrapping our components when we register the screens.

Wrapping up, we can summarize all of the above as follows. We created a set of actions and reducers that describes the application state after a navigation event occurs. We can conceptualize these state changes as a request for navigation. When we register our screens with the navigator, we use composition to wrap each component so that navigation occurs when we dispatch a relevant action. Each screen is thus wrapped in a container which listens for a these state changes and responds to the request by actually performing the navigation.

We found this architecture to be much more flexible, have a more sensible separation of concerns, and is overall much more developer-friendly than the out-of-the-box API.