A guide to React Native with Redux and Immutable.js

Gustavo Machado
Jan 24, 2017 · 6 min read

Redux is great, but in order to efficiently use redux in a React Native application, you have to make several decisions. In this article I share some of the libraries I’ve been comfortably using for a while. The stack that I propose supports:

  • Logging
  • Async actions
  • Immutable state (immutablejs)
  • Store persistence (async storage)

tl;dr; the libraries I’ll be using are:

  • react-redux
  • redux
  • redux-action-buffer
  • redux-actions
  • redux-immutablejs
  • redux-logger
  • redux-persist
  • redux-persist-immutable
  • redux-thunk

We’ll start with the simpler ones and move onto add a bit more complexity in more edge cases.

Adding logging to redux is pretty standard, and can easily be done with a middleware.

// enhanders.js
import {
applyMiddleware,
compose
} from ‘redux’;
import createLoggerMiddleware from ‘redux-logger’;
let middlewares = [];
if (__DEV__ === true) {
middlewares.push(createLoggerMiddleware({}));
}
export default compose(
applyMiddleware(…middlewares)
);

Now every time you run your react native application, you will see the state and actions in the console:

In order to support async flows in redux, there seems to be two clear options: redux-thunk and redux-sagas. There’s some very interesting debates about which is better in which scenarios, but as a rule of thumb I have come to the conclusion that while redux-sagas might be more flexible for complex scenarios, the lib tends to have a higher learning curve, and ends up adding complexity to the overall stack. So while, I think that it might very much worth it for bigger and more complex apps, I would make an attempt to stick with a simpler approach using redux-thunk. In the end, both solutions are acceptable in most scenarios and it almost always ends up being a matter of taste. In this case, I’ll add redux-thunk to the mix. Just like with logger, it’s a middleware.

import {
applyMiddleware,
compose
} from ‘redux’;
import createLoggerMiddleware from ‘redux-logger’;
import thunkMiddleware from ‘redux-thunk’;
let middlewares = [ thunkMiddleware ];
if (__DEV__ === true) {
middlewares.push(createLoggerMiddleware({}));
}
export default compose(
applyMiddleware(…middlewares)
);

Now that you have this middleware, you can define actions like this:

const init = () => ({ type: ‘APP_INIT’ })

But also like this:

const logout = (my_params) => (dispatch) => {
dispatch({type: ‘APP_LOGOUT_STARTED’});
// do your async stuff (ie: network calls)
// in some callback, you can keep dispatching:
dispatch({type: ‘APP_LOGOUT_ENDED’})
}

It’s basically a function that receives any parameters you may need, and returns a function that receives a “dispatch” function as a parameter.

There’s been some interesting debates around redux-thunk approach being a little bit cumbersome, and certainly dispatching multiple actions on each async action creator can become tedious and error prone when done manually. In order to avoid this, you can use a simple helper that takes an async function and dispatches actions accordingly as the function is executed. I personally have been using the following library (which uses redux-actions package):

import { createAction } from 'redux-actions';/**
* Creates an async action creator
*
* @param {String} TYPE the type of the action
* @param {Function} executeAsync the function to be called async
* @return {Funtion} the action creator
*/
export default function createAsyncAction(TYPE, executeAsync) {
const TYPE_STARTED = TYPE + '_STARTED';
const TYPE_FAILED = TYPE + '_FAILED';
const TYPE_SUCCEED = TYPE + '_SUCCEED';
const TYPE_ENDED = TYPE + '_ENDED';
let actionCreators = {
[ TYPE_STARTED ]: createAction(TYPE_STARTED),
[ TYPE_FAILED ]: createAction(TYPE_FAILED),
[ TYPE_SUCCEED ]: createAction(TYPE_SUCCEED),
[ TYPE_ENDED ]: createAction(TYPE_ENDED)
};
function create(...args) {return async (dispatch, getState) => {let result;
let startedAt = (new Date()).getTime();
dispatch(actionCreators[TYPE_STARTED]({ startedAt, ...args }));
try {
result = await executeAsync(...args, dispatch, getState);
dispatch(actionCreators[TYPE_SUCCEED](result));
}
catch (error) {
dispatch(actionCreators[TYPE_FAILED]({
errorMessage: error.message
}));
throw error;
}
let endedAt = (new Date()).getTime();
dispatch(actionCreators[TYPE_ENDED]({
endedAt: endedAt,
elapsed: endedAt - startedAt
}));
return result;
};
}
Object.assign(create, {
TYPE,
STARTED: TYPE_STARTED,
FAILED: TYPE_FAILED,
SUCCEED: TYPE_SUCCEED
});
return create;
}

This helper can be used like this:

createAsyncAction('CONFIG_ADD_CREDIT_CARD', async (userId, cc) => {
let token = await stripe.createCreditCard(cc);
await api.registerCreditCard(email, cc);
});

This will generate the following actions: CONFIG_ADD_CREDIT_CARD_STARTED , CONFIG_ADD_CREDIT_CARD_FAILED , CONFIG_ADD_CREDIT_CARD_SUCCEED and CONFIG_ADD_CREDIT_CARD_ENDED accordingly.

If at some point you console gets cluttered with too many actions being dispatch by these types of helpers, you can filter the actions that get logged with something like this:

middlewares.push(createLoggerMiddleware({
predicate: (getState, action) => !action.type.endsWith('_ENDED')
})

With redux, you are not supposed to modify the state directly, but rather generate a new copy of the state with the new (ehmm…) state. There are plenty of examples that use Object.assign and/or Array.slice(0) to generate new copies, but there are more convenient ways to do it. In this case, I’ll show you how to use immutable.js to generate these new copies. Immutable.js is a package maintained by facebook and provides an abstraction to dictionaries and arrays that can’t be mutated directly.

For mixing redux with immutable.js, we’ll use redux-immutablejs. This package takes care of the plumbing so that reducers can access an immutable.js instances. In order to use it, you simply usecreateReducer and combineReducers from redux-immutablejs instead of using the ones packaged with redux.

import { createReducer } from 'redux-immutablejs';
const reducer = createReducer(
{ loading: false},
{
'CONFIG_ADD_CREDIT_CARD_STARTED': (state) => state.merge({ loading: true })
'CONFIG_ADD_CREDIT_CARD_ENDED': (state) => state.merge({ loading: false })
}
);

One caveat to this, is that when using redux’s connect you’ll receive an instance of immutable.js, so you’ll have to convert it to a plain javascript object before setting the values to the props:

export default connect(
(state) => ({
config: state.get('config').toJS() //NOTICE: .toJS() here
}),
(dispatch, props) => ({
onAdd: () => dispatch(add())
})
)(Home);

Another caveat to this, is the redux-logger that we set in the first place. It will print the immutable object, instead of a plain javascript object. In order to fix this, you’ll have to do add configuration when you create the logger:

createLoggerMiddleware({
stateTransformer(state) {
return state.toJS()
}
});

At this point you’ll have what you’ll need for most of your scenarios. I would consider this the minimum set up for a react native app that uses redux. Adding reselect to the mix is definitely not a bad idea either.

In most apps, no matter how simple they are, you’ll most likely want to persist some part of the store between sessions. This is commonly used for things like:

  • Local settings, preferences, etc…
  • Authorization tokens, keys, etc…
  • Caching data (speeding loading times, etc…)
  • Keep progress (prevent users from typing things again)
  • Offline support (similar to caching)
  • etc…

Redux already provides a centralized store, which makes it easier to persist and “re-hydrate”, however as we’ll see, setting up all the pieces together will not be as trivial.

There’s a popular package to persist the store called redux-persist , however this package is more suitable for working with plain javascript objects. Since we are using immutable.js we’ll need to throw redux-persist-immutable to the mix.

The first we’ll have to do is add a redux enhancer:

import {
applyMiddleware,
compose
} from 'redux';
import { autoRehydrate } from 'redux-persist-immutable';
// set middlewares...let enhacers = [ autoRehydrate() ];export default compose(
applyMiddleware(...middlewares),
...enhacers
);

Second, we’ll have to initialize redux-persist-immutablejs.

import { createStore } from 'redux';
// react-native
import { AsyncStorage } from 'react-native';
import { persistStore } from 'redux-persist-immutable'
const store = createStore(
combinedReducers,
initialState,
composedEnhacers
);
//initialize redux-persist-immutable
persistStore(store, {storage: AsyncStorage});
export default store;

Up to this point, we have a working persistent store. But there’s one more thing we might need. In order to not override the current store with the one retrieved from the persistent storage (remember this is async in RN), you’ll have to either delay all dispatch calls until the store is retrieved, or you can “buffer” all actions until the store is retrieved. Even though the buffer alternative sounds complex (and it somewhat is), there’s a package that does precisely this redux-action-buffer. You can set it as yet another middleware:

//...
import createActionBuffer from 'redux-action-buffer'
import {REHYDRATE} from 'redux-persist/constants'
let middlewares = [ thunkMiddleware, createActionBuffer(REHYDRATE) ];export default compose(
applyMiddleware(...middlewares),
...enhacers
);

In this case REHYDRATE is a special action dispatched by redux-persist that notifies that the store has been successfully retrieved and re-hydrated.

Setting up a React Native project that uses redux from scratch may seem long and tedious (and it is!), but it’s important that you understand some of the decisions being made at every step. That’s why if you decide to use a starter kit, I strongly advise you make sure that it’s thoroughly documented, and that you understand the nature of the libraries being used under the hood.

The React Native Log

All things React Native — tutorials, experiments, tips & tricks, snippets

Gustavo Machado

Written by

The React Native Log

All things React Native — tutorials, experiments, tips & tricks, snippets