DEV Community

Gustavo Machado
Gustavo Machado

Posted on

React-navigation with redux and immutable.js

Last week I posted about using React Native with redux and Immutable.js, and looks like I was not alone when thinking that it certainly takes a while to get things right the first time. In this occasion, I'll expand on how to add react-navigation to the mix.

React-navigation

The beta release of react-navigation has been recently announced. This library builds on top of the experience from several other navigation libraries, perhaps most notably ex-navigation and navigation experimental (from RN). What I liked about the library is that it provides a simple yet powerful API. It allows to be used with minimum set up, but at the same time it supports integration with other libraries. In this case, we'll explore the integration with redux.

Do you need redux with react-navigation?

Before you continue with the article, you should really ask yourself whether you NEED to have your react-navigation integrated with redux. As a rule of thumb, I think you should try to avoid going this route. The only reason being, even without redux, you still get event sourcing out of the box, and you can still easily see and debug which actions where dispatched and the state before and after every navigation action.

As your application becomes more and more complex, I do believe that there are scenarios where it might pay off to have everything in redux. In my case, I was trying to do the following things:

  • Push screens upon conditions based on the application state.
  • Push screens based on the current application state and navigation stack.

For the record, these things can be done without redux, but as you'll see we get some nice flexibility and can keep code a little bit more decoupled when integrated with redux.

React-navigation with redux

From the react-navigation documentation we can see that integrating with redux is not all that complex. They already provide a nice helper for building the reducers, and preparing the props in case your children components need to use navigation themselves. Here is the code copy-&-pasted from the docs:

const AppNavigator = StackNavigator(AppRouteConfigs);

const appReducer = combineReducers({
  nav: (state, action) => (
    AppNavigator.router.getStateForAction(action, state)
  ),
  ...
});

@connect(state => ({
  nav: state.nav,
}))class AppWithNavigationState extends React.Component {
  render() {
    return (
      <AppNavigator navigation={addNavigationHelpers({
        dispatch: this.props.dispatch,
        state: this.props.nav,
      })} />
    );
  }
}

const store = createStore(appReducer);

class App extends React.Component {
  render() {
    return (
      <Provider store={store}>
        <AppWithNavigationState />
      </Provider>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

There are a few things missing, and we'll have to do a few adjustments before we can use it with immutable.js.

Adding Immutable.js to the mix

First, let's take care of the reducer. Apart from needing an initial state, our reducers manipulate immutable objects, so here's what our reducer might look like:

const initialState = Immutable.fromJS({  
  index: 0,  
  routes: [{  
    routeName: 'init',  
    key: 'init'  
  }]  
});  
const reducer = ( state = initialState, action ) => {  
  return state.merge(Navigator.router.getStateForAction(action, state.toJS()));  
}  
// now you can go ahead an do what is suggested in the docs:  
`const appReducer = combineReducers({  
  nav:reducer  
  //....  
});
Enter fullscreen mode Exit fullscreen mode

You may be wondering where that initalState is coming from. It turns out that redux-navigation uses a an internal state like this. Because we want to handle it ourside ourselves, we have to use the same format (if we want to use the helpers packaged with redux-navigation, which we do). I've taken this format from here: https://reactnavigation.org/docs/navigators/custom.

So as you can see, our initialState was constructed with Immutable.fromJS() method. Before we can tell the router's getStateForAction helper to give us the new state, we have to convert the current state to a plain javascript object with .toJS(). And last, we had to merge this into the older navigation state with state.merge.

Let's take a look at the connect:

const NavigatorWithState = ({dispatch, nav}) => {  
  return (  
    <Navigator navigation={addNavigationHelpers({  
      dispatch: dispatch,  
      state: nav,  
    })}/>  
  );  
}</pre>

<pre name="33f2" id="33f2" class="graf graf--pre graf-after--pre">export default connect(  
  state => ({  
    nav: state.get('nav').toJS()  
  })  
)(NavigatorWithState);
Enter fullscreen mode Exit fullscreen mode

Notice the .toJS() to make sure that the component gets a plain javascript object. Up to this point, your navigation should pretty much work, and you should be able to push scenes by dispatching vanilla redux actions.

Redux-persist

If you are using redux-persist or any other mechanism to persist your application's store, it may not be a bad idea to blacklist the navigation state from the persistence. The reason for this, is that you might want to run some validations and initialization logic before you actually take your users inside of your app. If this is the case, avoid rehydrating the navigation status from local storage.

With redux-persist this can be done with a blacklist option:

persistStore(store, {  
  blacklist: ['nav'],  
  storage: AsyncStorage  
});
Enter fullscreen mode Exit fullscreen mode

Leveraging Redux in your navigation

So now that you have gone through the travel of integrating react-navigation with redux, what kinds of things will you be able to do?

One of the things you can do, is use selectors to provide navigation state via props to your components. For example:

const selectors = {  
  currentRoute: state => {  
    let nav = state.get('nav').toJS();  
    return nav.routes[nav.index].routeName;  
  },  
  // selectedItem: state => {....  
  // etc...  
Enter fullscreen mode Exit fullscreen mode

And then in your connect you can use this to map state to props:

export default connect(  
  state => ({  
    currentRoute: router.selectors.currentRoute(state)  
  }),  
  dispatch => ({  
    init: () => dispatch(app.actions.init())  
  })  
)(Init);
Enter fullscreen mode Exit fullscreen mode

Another interesting thing you could do is dispatch new scenes from an action. Why would you want to do this? This could help decouple some navigation logic from your components. The only thing that I like about this, is that on the action creators side, everything navigation related are still plain objects, and with no further coupling to the navigation library (something harder to accomplish with other navigation libraries like react-navigator).

Suppose you want to display a nice error screen whenever there's a problem with an API call. You could do something like:

const add = (text) => {  
  return async (dispatch) => { //assuming you are using redux-thunk  
    dispatch({type: TODO_ADD_STARTED})  
    try {  
      let result = await todos.add(text);  
      dispatch({type: TODO_ADD_SUCCEEDED, payload: result});  
    }  
    catch (e) {  
      // this will display an 'error' scene  
      dispatch({  
        type:'Navigate',  
        routeName: 'error',  
        params: {  
          message: e.message || e  
        }  
      });  
    }  
  }  
}
Enter fullscreen mode Exit fullscreen mode

You can do a very similar thing by subscribing to the redux store's changes. Again, this would not require to couple this logic to the navigation library other than the actions which are plain objects.

Dispatching scenes from outside components may not seem natural, but it has one extra advantage that I like very much. Because action creators or store subscribers are decoupled from the navigation library, it is much simpler to unit test your navigation logic. I somewhat agree that this is not necessarily a common thing to do, but on the other hand, few times this has been as easy to test as it is right now with a set up like this.

Top comments (0)