The Movie DB / React Native Part 2: Navigation

May 2, 2017

This article is the second in the series for creating a simple app for discovering TV series based on The Movie DB API. In the previous article, we created all the components. We will now move on to navigation.

For navigation, we will use the NavigationExperimental component and Redux. If you want to learn more about the different navigation components, you can consult the navigation guide.

We will assume you already know Redux; otherwise, start by reading the Redux documentation.

Let's start by adding Redux to our project:

yarn add redux react-redux redux-thunk redux-logger

Next, create the following folders in the src folder

mkdir reducers store actions routes containers constants

Now that we have installed Redux and the folder structure is ready, we will define the two action types we will use for navigation. In the constants folder, create the ActionTypes.js file and add the following code:

ActionTypes.js

export const PUSH = 'PUSH'
export const POP = 'POP'

Let's create the actions. In the actions folder, create the navActions.js file and add the following code:

navActions.js

import { POP, PUSH } from '../constants/ActionTypes'
 
export function push(route) {
  return {
    type: PUSH,
    route,
  }
}
 
export function pop() {
  return {
    type: POP,
  }
}

So we have two actions:

  • pop: The action that will let us return to the previous screen when we are on a series detail screen.
  • push: The action that lets us navigate to the route passed as a parameter (in our case, the route for a series detail screen)

At the same time, we will create the route that lets us display a series detail screen.

In the routes folder, create an index.js file and add the following code:

index.js

export const detailRoute = item => {
  return {
    type: 'push',
    route: {
      key: 'detail',
      title: 'Detail',
      serieItem: item,
    },
  }
}

This route object contains different pieces of information:

  • type: The route action type
  • key: Defines which component should be rendered
  • title: Title of the view that will be rendered
  • serieItem: The data for the displayed series

Next, we will create our navigation reducer in the reducers folder.

navReducer.js

import { PUSH, POP } from '../constants/ActionTypes'
import { NavigationExperimental } from 'react-native'
const { StateUtils: StateUtils } = NavigationExperimental
 
const initialState = {
  index: 0,
  key: 'root',
  routes: [
    {
      key: 'home',
      title: 'Series',
    },
  ],
}
 
function navigationState(state = initialState, action) {
  switch (action.type) {
    case PUSH:
      if (state.routes[state.index].key === (action.route && action.route.key)) return state
      return StateUtils.push(state, action.route)
 
    case POP:
      if (state.index === 0 || state.routes.length === 1) return state
      return StateUtils.pop(state)
 
    default:
      return state
  }
}
 
export default navigationState

Here, we start by importing the action types we created earlier. Then we import NavigiationExperimental and define StateUtils. StateUtils is a helper that lets us call the basic routing methods. Finally, we define the initialState object; this object contains the default route we will start from.

Now let's create our rootReducer. Still in the reducers folder, add the index.js file

index.js

import { combineReducers } from 'redux'
import navReducer from './navReducer'
 
const rootReducer = combineReducers({
  navReducer,
})
 
export default rootReducer

We can now create our Redux store. Add the following file in the store folder.

configureStore.js

import { createStore, applyMiddleware, compose } from 'redux'
import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
import rootReducer from '../reducers'
 
const loggerMiddleware = createLogger()
 
export default function configureStore() {
  const middleware = [thunkMiddleware, loggerMiddleware]
  let store = compose(applyMiddleware(...middleware))(createStore)(rootReducer)
  return store
}

Now we can move on to creating the navigation component.

Let's create the navRoot component, which will be the app's entry point and will be rendered when the app loads.

navRoot.js

import React, { Component } from 'react'
import { View, StatusBar } from 'react-native'
import SerieList from './SerieListView'
import SerieDetail from './SerieDetail'
import { detailRoute } from '../routes'
 
import { BackAndroid, NavigationExperimental } from 'react-native'
 
const { CardStack: NavigationCardStack } = NavigationExperimental
 
class NavRoot extends Component {
  constructor(props) {
    super(props)
    this._renderScene = this._renderScene.bind(this)
    this._handleBackAction = this._handleBackAction.bind(this)
  }
  componentDidMount() {
    BackAndroid.addEventListener('hardwareBackPress', this._handleBackAction)
  }
  componentWillUnmount() {
    BackAndroid.removeEventListener('hardwareBackPress', this._handleBackAction)
  }
  _renderScene(props) {
    const { route } = props.scene
    switch (route.key) {
      case 'home':
        return (
          <SerieList
            showDetail={serieItem => {
              this._handleNavigate(detailRoute(serieItem))
            }}
          />
        )
        break
      case 'detail':
        return <SerieDetail goBack={() => this._handleBackAction()} serieItem={route.serieItem} />
        break
    }
  }
  _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.route)
        return true
      case 'back':
      case 'pop':
        return this._handleBackAction()
      default:
        return false
    }
  }
  render() {
    return (
      <View style={{ flex: 1 }}>
        <StatusBar backgroundColor={'black'} barStyle="light-content" />
        <NavigationCardStack
          direction="vertical"
          navigationState={this.props.navigation}
          onNavigate={this._handleNavigate.bind(this)}
          onNavigateBack={this._handleBackAction.bind(this)}
          renderScene={this._renderScene}
        />
      </View>
    )
  }
}
export default NavRoot

Let's look in detail at what happens in this component

  1. We import our components as well as the route that displays a series detail screen
  2. We import BackAndroid and NavigationExperimental from react-native. BackAndroid lets us manage the back button on Android
  3. We define the callbacks for the backAndroid event
  4. We define which component to return based on the route in the renderScene method
  5. We pass the necessary navigation methods to the components

Next, in the containers folder, create the NavRootContainer component

NavRootContainer.js

import { connect } from 'react-redux'
import NavigationRoot from '../components/navRoot'
import { push, pop } from '../actions/navActions'
 
function mapStateToProps(state) {
  return {
    navigation: state.navReducer,
  }
}
 
export default connect(mapStateToProps, {
  pushRoute: route => push(route),
  popRoute: () => pop(),
})(NavigationRoot)

Finally, to finish, replace the code in the index.js file at the root of the src folder with the following:

'use strict'
import React, { Component } from 'react'
import configureStore from './store/configureStore'
const store = configureStore()
 
import NavigationRootContainer from './containers/NavRootContainer'
import { Provider } from 'react-redux'
 
export default class App extends Component {
  constructor(props) {
    super(props)
  }
 
  render() {
    return (
      <Provider store={store}>
        <NavigationRootContainer />
      </Provider>
    )
  }
}

That's it: we are done with the navigation part.

Series navigation