Tackling React Native navigation with React Native Router Flux and Redux

Navigation is hard, but it is a necessary part of the application. One of the reasons why it is hard is because there are so many choices yet we are all trying to achieve the same goal. This post will help you quickly get started on how to properly structure your application using React Native Router Flux, and Redux.

I will not go into too much details on specific usages regarding each library, but rather give you clear guidelines on how to structure the project using simple examples.

Here are some of more popular choices of navigation:

  1. Navigator — This is probably the most popular choice for the starters. Navigator is available right out of the box from React Native.
  2. NavigationExperimental — This is also available right out of the box from React Native. This is the next version of Navigator. The Downside is that using this can be pretty tricky due to lack of documentation and examples. This library also requires the user to write a lot of boilerplate code to get started.
  3. ExNavigation — Exponent team has created this library. Don’t confuse this library with ExNavigator. This library uses NavigationExperimental but requires much less boilerplate code than using NavigationExperimental directly.
  4. React Native Router Flux — Similar to ExNavigation in a way that this library uses NavigationExperimental to do the navigation. Unlike any other libraries, it is the only one (that I know of) that uses JSX to create the structure of the scenes. Other libraries use JSON to structure the scenes but I find JSX to be much easier to reason about and to understand.

Of course, there are way more libraries that are worth looking into, but my guide from now on will specifically deal with RNRF (react-native-router-flux) and redux. If you have already decided to use different navigation library but is still using redux, you will still be able to hugely benefit from my guide.

If you want, you can skip straight to the full source code at
https://github.com/bosung90/react-native-router-flux-example

Tip 1: Connect your Router to redux store

According to redux, your entire state of the application should live within the redux store (Note: I don’t strictly follow this rule). One of the big advantages of connecting your Router to redux store is that your connected Components can easily know the current focused scene. Currently I am using this information for analytics and highlighting currently focused menu item in the drawer layout.

Luckily RNRF Router can be easily connected with redux by using connect from react-redux.

import { Router} from 'react-native-router-flux'
import { Provider, connect } from 'react-redux'
const ConnectedRouter = connect()(Router)
...
<Provider store={store}>
<ConnectedRouter>{/*Scenes*/}</ConnectedRouter>
</Provider>
...

Simply put, in the place of RNRF Router, use connected Router instead.

At this point, our redux store still does not consist of any navigation state. To do this we need to create a reducer for our navigation to decide what to store in our redux store.

import { ActionConst } from 'react-native-router-flux'
const DEFAULT_STATE = {scene: {}}
export default (state = DEFAULT_STATE, {type, scene})=> {
switch(type) {
// focus action is dispatched when a new screen comes into focus
case ActionConst.FOCUS:
return {
...state,
scene,
}
default:
return state
}
}

Whenever a new scene comes into focus due to push, pop, or replace, the action type of ActionConst.FOCUS will be dispatched with payload of scene. This scene is an object containing all the information regarding currently focused scene. If you are only interested in the key props of the Scene, then you can simply choose to only store scene.sceneKey

Tip 2: Connect all your Scenes to the redux store

This is probably the most confusing part about the RNRF Scene. Imagine you want to pass some state to your Scenes. Noticing that your Scenes are React Component, you may be tempted to pass down your state to your scene as props.

<Provider store={store}>
<ConnectedRouter>
<Scene key='root'>
<Scene key='home' component={Home} count={store.getState().home.count} />
</Scene>
</ConnectedRouter>
</Provider>

However, you will soon find out that although Home component does receive count props, it will never update (The count will always be the first state you passed down). This is because RNRF Scene are actually not a typical React Component, and simply returns null during its render(). RNRF will create scenes through the children props in the Router component. One way to bypass this is to pass a getter function as props that you can call from Home component every componentWillReceiveProps. However, I do not recommend this approach. A much better way is to connect each of your Scene to redux store. Please refer to github source code if you are still confused.

Tip 3: Scene with key is already defined warnings

When you encapsulate your Router with Provider from react-redux you may find your console to be constantly printed with the above warnings. This is because RNRF Scenes are not actually a React Component but simply used to create Scenes on initial render. You should create your Scenes once and only once. Luckily removing this warning is really simple.

import { Actions, Router, Scene } from 'react-native-router-flux'
import Home from './Home'
const Scenes = Actions.create(
<Scene key='root'>
<Scene key='home' component={Home}/>
</Scene>
)
...
<Provider store={store}>
<ConnectedRouter scenes={Scenes}/>
</Provider>
...

You can see that instead of making Scene components child of ConnectedRouter, I am instantiating it outside of the class and simply providing a reference to it to my ConnectedRouter with props scenes.

Tip 4: Decouple Reducers and Scene Components

I have seen many badly structured projects where every time they create a new page, they also create a new reducer, presentational component, container component, and styling. The project soon becomes filled with duplicated logics and maintenance hell. Reusing presentational component is pretty easy and easy to understand, therefore I have decided to focus only on reducers in tip 4 and action creators in tip 5.

import {combineReducers} from 'redux'
import nav from './navReducer'
import home from './homeReducer'
export default combineReducers({
nav,
home,
})
export const getNav = ({nav}) => nav
export const getHome = ({home}) => home

Here you can see I am creating a combinedReducer. However, pay special attention to getNav and getHome. There is a really good reason for this. Imagine that my Home component contains a simple counter. My homeReducer.js code is as follows:

const DEFAULT_STATE = {count: 0}
export default (state = DEFAULT_STATE, {type, payload})=> {
switch(type) {
case 'INCREMENT':
return {
...state,
count: state.count+1,
}
case 'DECREMENT' :
return {
...state,
count: state.count-1,
}
default:
return state
}
}

It is true that after I connect my Home component, I can simply retrieve count using state.home.count however, this will couple my Home component with homeReducer.js. Simply put, your Home component does not care that count is within state.home. By using getHome mapReduce, not only I have successfully decoupled Home component with homeReducer, now I can combine multiple reducers to provide the appropriate data to my presentational components.

import Home from './Home' // Presentational Component
import {connect} from 'react-redux'
import {getNav, getHome} from '../reducers'
const mapStateToProps = (state, props)=> {
return {
...getNav(state),
...getHome(state),
}
}
export default connect(mapStateToProps)(Home)

Tip 5: Composable and Decoupled Action Creators

Normally you dispatch actions within container components like following:

const mapDispatchToProps = (dispatch, ownProps)=>({
incrementCount: ()=>{
dispatch({type: 'INCREMENT'})
},
decreaseCount: ()=>{
dispatch({type: 'DECREMENT'})
},
})
export default connect(mapStateToProps, mapDispatchToProps)(Home)

However, since dispatching within mapDispatchToProps is such a common pattern, you can simplify it even further as following:

const mapDispatchToProps = {
incrementCount: ()=>({
type: 'INCREMENT'
}),
decrementCount: ()=>({
type: 'DECREMENT'
}),
}
export default connect(mapStateToProps, mapDispatchToProps)(Home)

You may have noticed that you no longer have access to ownProps. This is actually a good thing, since relying on ownProps will couple your actionCreators to your container component, which you do not want. Luckily this is a very easy problem to solve since ownProps is available to Presentational Component (in this case Home), and you can simply pass it as parameters when you call actions.

Now we can further improve the code by decoupling actions and container component as following:

import * as actions from './actions'
const mapDispatchToProps = {
...actions
}
export default connect(mapStateToProps, mapDispatchToProps)(Home)
// actions.js
export const incrementCount = ()=>({
type: 'INCREMENT'
})
export const decrementCount = ()=> ({
type: 'DECREMENT'
})

A huge benefit of decoupling action creators and container component is that not only you can re-use action creators in other container components, but also you can combine action creators from multiple sources.

If you need to conditionally dispatch action(s) based on the current state of the application, take advantage of redux-thunk. Simply return a function instead of action object like following:

// actions.js
export const incrementCountThunk = ()=>(
(dispatch, getState)=>{
const {count} = getHome(getState())
if(count < 10) {
dispatch({type: 'INCREMENT'})
}
}
)

You may be tempted to not use redux-thunk and pass all the required state of application from Presentational Component as parameters when you call the actions, however, this creates unnecessary coupling between Presentational Component and Container Component. This means it will be harder to re-use Presentational Components.

Tip 6: API calls inside action creators

Previous tip shows how action creators inside actions.js file is used to dispatch actions. This is also a good place to put any API calls such as network calls or navigation push/pop.

// Home/actions.js
import {Actions} from 'react-native-router-flux'
export const handleCard = ()=>{
Actions.card({title: 'Custom Card Title'})
// Redux require you to return an object with type
return {type: 'CardPush'}
}
// Home/Home.js
export default (props) => {
const {handleCard} = props
return (
<View style={{flex:1, justifyContent: 'center', alignItems: 'center'}}>
<Text onPress={()=>handleCard()}>Push New Card Scene</Text>
</View>
)
}

Actions.card() will push a new Scene with key=’card’ into the nav stack. You can pass additional props to the new scene by passing it as parameter. For more configurations regarding what options you can use refer to react-native-router-flux github readme

Thank you for reading.

Please comment for any questions or suggestions, feedback is also welcome.