You are on page 1of 18

A stately guide to React Navigation

with Redux
Daniel Merrill Follow
Jan 29, 2018 · 10 min read

Note: This is part 5 in a series on React Navigation. Check out my posts on


Getting Up and Running, Styling Navigators, an introduction to Stack,
Tab, and Drawer Navigators, and Custom Transition Animations.

R eact Navigation’s default routing behavior works perfectly for many


mobile apps, but sometimes you need more customization than it
provides out of the box. You can unlock useful functionality by
manually managing navigation state in Redux.

An app’s navigation state is typically represented as a stack or a tree


and can be thought of as a record of the past (“how did I get to this
screen?”) as well as instructions for the present (“the news feed screen
should be rendered because it’s on top of the navigation stack”). In this
post we’ll create a new app, wire up a Redux store, connect our
navigator to that store, and learn how to preserve the current state of
our app whenever it closes.

By default, React Navigation manages its state internally, but keeping


navigation state in our own Redux store offers a few advantages. We
can save navigation state from session to session using tools like redux-
persist, and can create our own navigation reducer to customize
behavior. For example, you may want to ensure that only a single
instance of a screen can exist on the navigation stack at any given
moment. As with everything, there is a tradeoff — in this case we’re
adding boilerplate and complexity to our app.

React Navigation provides some documentation on integrating Redux,


but it’s pretty sparse and could benefit from a more thorough example,
which we’ll build here. This tutorial assumes you have some familiarity
with both React Navigation and Redux, but I’ll keep it as beginner-
friendly as possible. Let’s dive in.

. . .

Initialization
I’ll start a new project from scratch:

react-native init ReactNavigationRedux

We’ll add the two main libraries we’ll be working with: React
Navigation and Redux. We’ll also add a few helper libraries.

yarn add react-navigation


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

• react-navigation: Handles screen-to-screen navigation and


associated UI

• redux: Manages a single app-wide state store

• react-redux: Provides bindings for react components to hook into


Redux

• redux-logger: A handy tool for inspecting the state of our store


from the debugger

I’m going to breeze over the part where I create a basic app with a
StackNavigator , a few routes with corresponding screen

components, and set up a UI with basic routing and param passing. If


any of this sounds foreign to you, check out my tutorial on getting Up
and Running with React Navigation.

Here’s our starting Navigator.js :


1 import React, { Component } from 'react'
2
3 import Feed from './components/Feed'
4 import ItemDetail from './components/ItemDetail'
5
6 import { StackNavigator } from 'react-navigation'
7
8 export const Navigator = new StackNavigator({
9 Feed: { screen: Feed },
10 ItemDetail: { screen: ItemDetail },
11 },{
12 initialRouteName: 'Feed',
13 })
14
15 class Nav extends Component {
16 render() {

Our default export will be a navigator wrapped in a component. We’ll also export the bare
StackNavigator as a named export. It’ll be clear why we’re doing this in a bit.
Our app has a feed and each item has a detail screen

What is Redux?
Redux is a library used to maintain a single “source of truth” for an
application’s data. Understanding its inner workings is beyond the
scope of this post, and if you’re already comfortable with Redux feel
free to skip this section, but I’ll give an overview for those who could
use a refresher.

The concept isn’t too complicated but requires the coordination of a few
elements. A single “store” object is created to hold whatever data we
want. To ensure that our store’s data is predictable, we aren’t allowed to
directly alter or even access it.

In order to alter data, we “dispatch” an “action” object to the store that


contains all the information the store needs in order to make the
change. The “action” object we dispatch must include a type property
and may include a payload of extra data:
const action = {
type: 'USER_LOGGED_IN',
payload: { user: 'Daniel' }
}

store.dispatch(action)

Then, we listen for these actions in a reducer function and return a new
state object based on the type of the action, payload data, and/or
previous state of the store:

1 function(state = {}, action) {


2 switch(action.type) {
3 case 'USER_LOGGED_IN':
4 return {
5 ...state,
6 user: action.payload,
7 }
8 default:

Note: Never mutate old state. Always return a new object instead.

To access our data, we must call store.getState() . After the action


above is dispatched, store.getState().user will be 'Daniel' .

Once our store is set up, we can inject up-to-date state from the store
into any component we like using the the connect function from react-
redux. Maintaining this “single source of truth” solves many headaches
that arise in passing state around an application.

To recap:

• store : Single repository for all data

• dispatch : A method of the store that we use to pass actions to


a reducer in order to update stored data

• action : An object with a type property that describes


something that should result in a change of state.

• reducer : A function that receives the current Redux state and the
action dispatched, and returns a new state object that replaces the
current state in the store.
• connect : A higher-order function from react-redux that can wrap
one of our components. connect takes up to two arguments,
usually named mapStateToProps and mapDispatchToProps . We
use these functions to tell Redux which pieces of state to pass to
our component as props , and also to give us a convenient
reference to the store’s dispatch method. See examples here.

Check out the cartoon guides to Flux and Redux for a friendly
walkthrough of this pattern.

Add Redux to our app


In order to initialize Redux, we’ll need to create a store with a
corresponding reducer. I want to get all the basic pieces in place before
I begin fleshing out my reducer, so I’ll make a dummy reducer and we’ll
come back to it. This one does nothing except return whatever state is
currently held in the store (or an empty object if we don’t have any
current state):

1 export default (state = {}, action) => {


2 return state
3 }

Dummy reducer

We’ll import our reducer into App.js and set up our Redux store:

1 import React, { Component } from 'react';


2 import { createStore, combineReducers, applyMiddleware } fr
3 import { Provider } from 'react-redux'
4 import logger from 'redux-logger'
5
6 import navigation from './src/reducer'
7
8 import Navigator from './src/Navigator'
9
10 const reducer = combineReducers({ navigation })
11 const store = createStore(reducer, applyMiddleware(logger))
12
13 export default class App extends Component {
14 render() {

Note that our app is wrapped in a <Provider> component. Under the


hood, Provider uses react context to allow any child component to access
the store.

Connect our Navigator


We’re now ready to connect our navigator to Redux. We’ll use the
connect function from react-redux to include our Redux navigation
state on our Navigator component’s props.

Then, we’ll add a navigation prop to our Navigator using a helper


function from React Navigation called addNavigationHelpers . This will
replace the default navigation prop that React Navigation passes to
its child components:

1 import React, { Component } from 'react'


2 import { connect } from 'react-redux'
3 import { StackNavigator, addNavigationHelpers } from 'react
4
5 import Feed from './components/Feed'
6 import ItemDetail from './components/ItemDetail'
7
8
9 export const Navigator = new StackNavigator({
10 Feed: { screen: Feed },
11 ItemDetail: { screen: ItemDetail },
12 },{
13 initialRouteName: 'Feed',
14 })
15
16 class Nav extends Component {
17 render() {
18 return (
19 <Navigator navigation={addNavigationHelpers({
20 dispatch: this.props.dispatch,
hi i i
Since we wrapped our Navigator in a component, we can `connect` it to Redux and inject our
navigation state and our store’s `dispatch` method (`dispatch` is automatically added to our props
when we use `connect`).

For those using React Navigation 1.0.0 and above please follow the
additional setup steps:
https://github.com/computerjazz/ReactNavigationRedux/issues/1#issue
comment-372718115

Give it a refresh and…


Whoops! We’re breaking things already. Unfortunately, a few changes
have to land at the same time in order to get up and running, so we’ll
have to go through a few rounds of breaking and fixing our app.

In this case, addNavigationHelpers is expecting a “valid” state object


that contains an index and a routes array, which we need to provide
now that we’ve replaced the default navigation prop. The state

we’re currently passing to it is the result of our dummy reducer, which


we initialized as an empty object.
We’ll fix this by providing our reducer with a valid default
initialState . This could be a complex tree if we want to initialize our
app en media res, but we’ll feed it an object that represents the initial
route we defined in our StackNavigator .

Thankfully, we don’t need to manually create this object—React


Navigation can do this for us. Remember earlier when we exported our
bare Navigator as a named export? We’ll now import it into our
reducer, along with the NavigationActions object from React
Navigation.

When we call Navigator.router.getStateForAction() with an action of


type NavigationActions.Init , we get back an object that represents
our initial navigation state:

1 import { Navigator } from './Navigator'


2 import { NavigationActions } from 'react-navigation'
3
4 const initialAction = { type: NavigationActions.Init }
5 const initialState = Navigator.router.getStateForAction(init
6
7 export default (state = initialState, action) => {
Remember, we exported our plain Navigator as a named export in Navigator.js, separate from the
component-wrapped version.

If we log initialState we see an object with a shape that will become


familiar:

Our navigation stack is initialized at index 0, which is the route with name ‘Feed’

Now, we can refresh our app and it won’t crash. However, tapping on a
list item no longer routes to the correct screen:
This is because React Navigation now expects us to explicitly provide
navigation state, and our current dummy reducer only returns the state
that’s already in the store. Since we defaulted our state.navigation to
initialState we’re stuck on the initial screen. Let’s go back and fix
that now.

Before we replaced the navigation prop, React Navigation handled all


calls to props.navigation.navigate() internally. Now, when we call
props.navigation.navigate() from within our component, an action is
dispatched to our store (remember that we provided our own store’s
dispatch method to our navigator within addNavigationHelpers ).

Let’s update our reducer to return the correct navigation state. Since we
have the handy getStateForAction function that returns a navigation
state object based on the current state and the passed-in action, all we
have to do is change one line,—the return value:
1 import { Navigator } from './Navigator'
2 import { NavigationActions } from 'react-navigation'
3
4 const initialAction = { type: NavigationActions.Init }
5 const initialState = Navigator.router.getStateForAction(ini
6
7 export default (state = initialState, action) => {
8 // Our Navigator's router is now responsible for

…and our app is back to behaving as expected:

Let’s take a moment to recap what we did:

• Created a reducer that outputs new navigation state based on its


current state and a passed-in action

• Created a store using that reducer


• Wrapped our app in a <Provider> and gave it a reference to our
store

• Connected our Navigator to our store by replacing the


navigation prop using the addNavigationHelpers function from
React Navigation and the connect function from react-redux. We
provided our store’s navigation state and dispatch method.

. . .

The Payoff
So…did we just spend a lot of effort to end up back where we began?
Nope! Let’s explore some benefits of manual state management. I
would argue the main win is the ability to store state with redux-persist
and load the app in the same state we left it, but there are other
benefits too.

The following customizations feel slightly hacky to me, but I have found
various uses for them. The main takeaway here is that navigation
behavior can be thought of as the result of changes in navigation state.
Therefore, by modifying state, we modify behavior. Sounds pretty
React-y, no?

Replacing the top route


I’m going to alter my ItemDetail screen by adding a link to another
random ItemDetail screen. However, I always want my ‘Back’ button
to take me back to my item list, no matter how many links I’ve followed
—i.e. I want to replace the top of my stack instead of add to it.

To do this, I’ll add replace: true to my navigation params, then test


for a replace param in our reducer, and if params.replace === true ,
I’ll swap out the top item on the navigation stack with the new item:

1 goToRandomItem = () => {
2 const items = ['one', 'two', 'three', 'four', 'five']
3 const rand = Math.floor(Math.random() * 5)
4 this.props.navigation.navigate('ItemDetail', { item: ite
5 }

Here’s our replace-enabled reducer. We’ll implement a ‘replace’ by


splicing one element out of our routes array and then decrementing
the index :
1 import { Navigator } from './Navigator'
2 import { NavigationActions } from 'react-navigation'
3
4 const initialAction = { type: NavigationActions.Init }
5 const initialState = Navigator.router.getStateForAction(ini
6
7 export default (state = initialState, action) => {
8 let newState = Navigator.router.getStateForAction(action,
9
10 if (action.params && action.params.replace) {
11 // In order to replace the previous route
12 // we'll remove the item at index - 1 and then decremen
13 const { index } = newState

And the result:

Since the index and stack size remain the same, we no longer see an animation to a new screen.
Active check
What if we want a screen that does some work every few seconds
(make a network request, etc), but only if the user is actively viewing
that screen? It can be nice to know if a screen is active and
unfortunately React Navigation doesn’t ship with this functionality*.
We can add it in our reducer:

1 import { Navigator } from './Navigator'


2 import { NavigationActions } from 'react-navigation'
3
4 const initialAction = { type: NavigationActions.Init }
5 const initialState = Navigator.router.getStateForAction(ini
6
7 export default (state = initialState, action) => {
8 let newState = Navigator.router.getStateForAction(action,
9
10 // Replace top route with new route
11 if (action.params && action.params.replace) {
12 // In order to replace the previous route
13 // we'll remove the item at index - 1 and then decremen
14 newState.routes.splice(newState.index - 1, 1)
15 newState.index--
16 }
17
18 // Insert 'active' prop on route
19 newState.routes.forEach((route, i) => {

Now we’ll get props.navigation.state.params.active on each screen


that gets rendered. In the following example, I’ve kicked off a periodic
console.log in componentDidMount on the main ‘Feed’ screen.
However, it will only log if its screen is marked as active , so I’ll stop
seeing logs as soon as I navigate away to any ‘ItemDetail’ screen.

1 _interval = null
2
3 componentDidMount() {
4 this._interval = setInterval(this.periodicUpdate, 1000)
5 }
6
7 periodicUpdate = () => {
8 if (this.props.navigation.state.params.active) {
*At the time of writing, a PR has just been merged into React Navigation
that adds event listening and may solve this issue. Stay tuned.

. . .

Persistence
I’ve mentioned redux-persist a few times in this post, which is a
package used to save the contents of our Redux store to local storage.
Let’s go ahead and add it to our app:

yarn add redux-persist

Usually, we’d use combineReducers from Redux to segment our store


into constituent slices, but redux-persist provides
combinePersistReducers which will handle that for us, plus it gives our
reducers the power to write to disk. We’ll also create a simple config

object and pass it to combinePersistReducers along with our reducers


(we only have a navigation reducer, but a real app probably has many
others). Then, we’ll create our store and pass it into persistStore()

to enable persistence.

Here’s our final App.js with redux-persist:


1 import React, { Component } from 'react';
2 import { createStore, combineReducers, applyMiddleware } fr
3 import { Provider } from 'react-redux'
4 import logger from 'redux-logger'
5
6 import navigation from './src/reducer'
7 import { persistStore, persistCombineReducers } from 'redux
8 import storage from 'redux-persist/lib/storage' // default:
9
10 import Navigator from './src/Navigator'
11
12 const config = {
13 key: 'primary',
14 storage
15 }
16
17 // Use the reducer combining function provided by redux-per
18 const reducer = persistCombineReducers(config, { navigation
19 const store = createStore(reducer, applyMiddleware(logger))
20 persistStore(
21 store,
22 null,
23 () => {
24 store.getState() // if you want to get restoredState

Now, we can navigate around as much as we like, close the app, and
when we open it we will be right where we left off. Plus, all of our other
Redux state will be persisted too!

Final code here:

• With redux-persist:
https://github.com/computerjazz/ReactNavigationRedux/tree/d
m-reduxPersist

• Without redux-persist:
https://github.com/computerjazz/ReactNavigationRedux

Other posts in this series:


• Up and Running With React Navigation

• React Navigation: I like your style

• Custom Transitions in React Navigation


• Stacks, Tabs, and Drawers…oh my!

Notes:
• If you’re getting a redbox error regarding undefined
addListeners , follow the instructions here:
https://github.com/computerjazz/ReactNavigationRedux/issues/
1#issuecomment-372718115

• redux-persist was created by Async co-founder Zack. Go read his


post about it!

• Because we fed our dispatch to our Navigator, we also have


access to it on props.navigation.navigate.dispatch() . Bonus!

• We didn’t use Redux to its full potential—in fact we relied entirely


on getStateForAction and didn’t explicitly listen for any actions
in our reducer! Adding custom actions and their resulting state
modifications can be an exercise for the reader 🙂.

• You may need to implement Android hardware back button


functionality on your own: https://github.com/react-
navigation/react-navigation/issues/3181

. . .

Async builds high performance, reliable, and cost-effective applications


by combining technical expertise and deep knowledge of industry
trends.

For more information on development services, visit asy.nc

You might also like