React Native’s NavigationExperimental with Redux

Satyajit Sahoo
5 min readJun 4, 2016

--

Navigation is a core part of any application. React Native has a `<Navigator />` component to help with navigation. A major flaw in the API is that the component is stateful. So you don’t own the navigation state. Not owning the navigation state has many downsides, e.g. — it’s impossible to add custom navigation actions, easily track navigation actions (say for analytics) and many other. I have been playing with the new experimental `NavigationExperimental` API which has been available in React Native for some time, which aims to solve this. We’ll see how to use it to store the navigation state in a redux store.

Note: I’ll be using 0.27-rc2 in my example. But since the API is not stable yet, it might change in the next version.

Project Structure

We’ll use the following project structure.

.
├── android/
├── ios/
├── node_modules/
├── src
│ ├── actions
│ │ └── NavigationActions.js
│ ├── components
│ │ └── NavigationRoot.js
│ ├── constants
│ │ └── ActionTypes.js
│ ├── containers
│ │ ├── NavigationRootContainer.js
│ │ └── RootContainer.js
│ ├── reducers
│ │ ├── index.js
│ │ └── navigation.js
│ ├── store
│ │ └── configureStore.js
├── index.android.js
├── index.ios.js
└── package.json

The Actions

We’ll create some actions so that we can change the navigation state from our components. Let’s add two actions, `push` and `pop`.

Let’s create a file called `src/constants/ActionTypes.js` which contains our actions where we’ll define the constants for our actions,

export const PUSH_ROUTE = ‘PUSH_ROUTE’;
export const POP_ROUTE = ‘POP_ROUTE’;

Now let’s write the action creators under `src/actions/NavigationActions.js`,

import { PUSH_ROUTE, POP_ROUTE } from ‘../constants/ActionTypes’;export function push(route) {
return {
type: PUSH_ROUTE,
payload: route,
};
}
export function pop() {
return {
type: POP_ROUTE,
};
}

The Reducer

The reducers will create the new navigation state when a component dispatches a navigation action. We’ll need to define the initial navigation state, which will have one route (the home route) and handle the `push` and `pop` actions in the reducer.

Let’s write the reducer in `src/reducers/navigation.js`,

import { PUSH_ROUTE, POP_ROUTE } from ‘../constants/ActionTypes’;const initialState = {
index: 0,
key: ‘root’,
children: [
{
key: ‘home’,
title: ‘Welcome home’,
},
],
};
export default (state = initialState, action) => {
const {
index,
children,
} = state;
switch (action.type) {
case PUSH_ROUTE:
return {
…state,
children: [
…children,
action.route,
],
index: index + 1,
};
case POP_ROUTE:
return index > 0 ? {
…state,
children: children.slice(0, children.length — 1),
index: index — 1,
} : state;
default:
return state;
}
};

Here we’re handling the `push` and `pop` actions and add and remove the route from our navigation stack respectively. It’s easy to add more actions and manipulate the state freely as we’re just dealing with plain objects and arrays.

Note that we’ll need to name the key of the route array as `routes` instead of `children` in next version of React Native.

Let’s also create a file `src/reducers/index.js` which can include other reducers to be used by the store.

We’ll be storing our navigation state under a key named `navigation` in our state object, so our root reducer looks like the following,

import { combineReducers } from ‘redux’;
import navigation from ‘./navigation’;
const rootReducer = combineReducers({
navigation,
});
export default rootReducer;

The Store

Now let’s wire up the things, and actually use the reducer in our store. Let’s create a file `src/store/configureStore.js` with the following content,

import { createStore } from ‘redux’;
import rootReducer from ‘../reducers’;
export default function configureStore(initialState) {
const store = createStore(rootReducer, initialState);
if (module.hot) {
module.hot.accept(() => {
const nextRootReducer = require(‘../reducers/index’).default;
store.replaceReducer(nextRootReducer);
});
}
return store;
}

We’ve a very simplistic `configureStore` function right now. In a real world project, it’ll likely have a little more here as we start to use middlewares and store enhancers.

The Navigation Component

Let’s create our `NavigationRoot` component in `src/components/NavigationRoot.js` which will take care of rendering our scenes. We’ll use the `CardStack` component provided by `NavigationExperimental` so we have nice animations. Otherwise we could just check the current route and return the respective component.

import React, { Component } from ‘react’;
import {
BackAndroid,
NavigationExperimental,
Text,
} from ‘react-native’;
const {
CardStack: NavigationCardStack,
} = NavigationExperimental;
export default class NavigationRoot extends Component {
constructor(props) {
super(props);

this._renderScene = this._renderScene.bind(this);
this._handleNavigate = this._handleNavigate.bind(this);
this._handleBackAction = this._handleBackAction.bind(this);
}
componentDidMount() {
BackAndroid.addEventListener(‘hardwareBackPress’, this._handleBackAction);
}
componentWillUnmount() {
BackAndroid.removeEventListener(‘hardwareBackPress’, this._handleBackAction);
}
_renderScene({ scene }) {
const {
index,
navigationState,
} = scene;
// render your scene based on the route (navigationState)
return <Text>Current route: {navigationState.key}</Text>;
};
_handleBackAction() {
if (this.props.navigation.index === 0) {
return false;
}
this.props.popRoute();
return true;
};
_handleNavigate(action) {
switch (action && action.type) {
case ‘push’:
this.props.pushRoute(action.payload);
return true;
case ‘back’:
case ‘pop’:
return this._handleBackAction();
default:
return false;
}
};
render() {
return (
<NavigationCardStack
direction=’vertical’
navigationState={this.props.navigation}
onNavigate={this._handleNavigate}
renderScene={this._renderScene}
/>
);
}
}

The `_renderScene` method is where we need to render our component based on the route. You get the current route in the `navigationState` key as shown in the code. You can return a react element matching the route. I’m leaving it blank here as it’s mostly dependent on your app.

We are also handling Android’s back button here, so pressing the back button navigates back in the app instead of exiting it.

The actions emitted by `NavigationCardStack` are `push`, `pop` and `back`, and not the constants we defined earlier, so we are just mapping them to our actions in `_handleNavigate`. For example, when you perform a gesture to navigate back, the `NavigationCardStack` will call the `onNavigate` function with the `back` action.

The `pushRoute`, `popRoute` and `navigation` props come from the container which we’ll write next.

The Navigation Container

Let’s create our container component under `src/containers/NavigationRootContainer.js` which will look something like the following,

import { connect } from ‘react-redux’;
import NavigationRoot from ‘../components/NavigationRoot’;
import { push, pop } from ‘../actions/NavigationActions’;
function mapStateToProps(state) {
return {
navigation: state.navigation,
};
}
function mapDispatchToProps(dispatch) {
return {
pushRoute: route => dispatch(push(route)),
popRoute: () => dispatch(pop()),
};
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(NavigationRoot);

Wiring it up

Now let’s render our `NavigationRootContainer` and we are done. Let’s import it in our `index.ios.js` and/or `index.android.js` file and render it,

import React from ‘react’;
import { AppRegistry } from 'react-native';
import configureStore from ‘./src/store/configureStore’;
import NavigationRootContainer from ‘./src/containers/NavigationRootContainer’;
const store = configureStore();const App = () => {
return (
<Provider store={store}>
<NavigationRootContainer />
</Provider>
);
};
AppRegistry.registerComponent('App', () => App);

If everything goes well, the app should render. Now you can dispatch actions to your redux store to navigate between routes, and do all sorts of manipulation in your reducer. Happy navigating 🍺

--

--

Satyajit Sahoo

Front-end developer. React Native Core Contributor. Codes JavaScript at night. Crazy for Tacos. Comic book fanatic. DC fan. Introvert. Works at @Callstackio