A stately guide to React Navigation with Redux

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.

React 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:

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:

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):

Dummy reducer

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

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:

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#issuecomment-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:

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:

…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:

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:

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:

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.

*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:

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:

Other posts in this series:

Notes: