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
:
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:
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 datadispatch
: A method of thestore
that we use to pass actions to a reducer in order to update stored dataaction
: An object with atype
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 namedmapStateToProps
andmapDispatchToProps
. We use these functions to tell Redux which pieces of state to pass to our component asprops
, and also to give us a convenient reference to the store’sdispatch
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):
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:
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:
If we log initialState
we see an object with a shape that will become familiar:
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 thenavigation
prop using theaddNavigationHelpers
function from React Navigation and theconnect
function from react-redux. We provided our store’s navigation state anddispatch
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:
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:
- With redux-persist: https://github.com/computerjazz/ReactNavigationRedux/tree/dm-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 onprops.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