Estado inmutable con Redux e Immutable.js

Redux nos propone tratar nuestro estado como inmutable. Sin embargo los objetos y array en JavaScript no lo son, lo que puede causar que mutemos directamente el estado por error.

Immutable.js es una librería creada por Facebook para usar colecciones de datos inmutables como listas, mapas, sets, etc. Usándolo con Redux nos permite expresar nuestro estado como colecciones de Immutable.js para evitarnos estos posibles problemas al mutar datos.

Usándolo en un reducer

La primera forma de usar Immutable.js en Redux es usándolo directo en un reducer. Simplemente definiendo el estado inicial como una colección inmutable y luego modificándolo según la acción despachada.

import { Map as map } from 'immutable';
function reducer(state = map(), { type, payload }) {
switch (type) {
case 'ADD': {
return state.set(payload.id.toString(), map(payload));
}
default: {
return state;
}
}
}
export default reducer;

De esta forma podemos empezar a hacer uso de Immutable.js. Un pequeño detalle al usar mapas inmutables es que el key usado debe ser siempre un string, puede ser un número, pero por experiencia esto pueda dar errores de que Immutable.js no encuentre el valor al hacer colection.get(1), por esa razón cuando agregamos el dato a nuestro mapa usamos .toString() sobre el ID para evitarnos este problema.

Combinando reducers

Aunque es posible tener un único reducer para toda la aplicación, a medida que esta crece lo común es empezar a dividirlo en múltiples reducers y usar redux.combineReducers para unirlos en uno solo que usamos al crear el Store.

import { combineReducers } from 'redux';
import data from './reducers/data.js';
export default combineReducers({
data,
});

De esta forma nuestro estado ahora es un objeto con una propiedad data la cual posee nuestra colección inmutable, pero ¿Qué pasa si queremos que todo nuestro estado sea un conjunto de colecciones inmutables anidadas?

Combinando reducers con Immutable.js

Si decidimos tratar todo el estado como una colección inmutable debemos entonces hacer uso de redux-immutable. Esta librería nos ofrece una función combineReducers personalizada la cual funciona con exactamente la misma API que la oficial de Redux, por lo que hacer el cambio de una a otra consiste en cambiar de donde importamos la función.

import { combineReducers } from 'redux-immutable';
import data from './reducers/data.js';
export default combineReducers({
data,
});

Como vemos simplemente pasamos de importar desde redux a hacerlo desde redux-immutable, con ese simple cambio estamos usando Immutable.js en todo nuestro store, ahora cuando conectemos nuestros componentes a este podemos usar una sintaxis 100% de Immutable.js.

function getItem(state, props) {
return state
.get('data')
.get(props.id.toString())
.toJS(),
}

Ese selector por ejemplo se encarga de traerse del mapa de datos el item con el ID recibido como prop y devolverlo convertido a un objeto de JS común que podemos recibir en un componente y usarlo sin problemas.

Usándolo con react-router-redux

Resulta que la librería react-router-redux usada para combinar Redux y React Router no se lleva bien con un estado inmutable como el que crearíamos al usar redux-immutable.

Para poder usarlos juntos necesitamos crear nuestro propio reducer encargado de guardar y actualizar los datos de la ruta actual.

import { fromJS } from 'immutable';
import { LOCATION_CHANGE } from 'react-router-redux';
const initialState = fromJS({
locationBeforeTransitions: null,
});
export default function routeReducer(state = initialState, action) {
if (action.type === LOCATION_CHANGE) {
return state.merge({
locationBeforeTransitions: action.payload,
});
}
return state;
}

Con eso ya tenemos nuestro propio reducer para rutas. Ahora necesitamos cambiar el selector que usa react-router-redux para que convierta los datos a un objeto normal de JS. Para eso pasamos un tercer argumento syncHistoryWithStore.

import { browserHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
const history = syncHistoryWithStore(browserHistory, store, {
selectLocationState(state) {
return state.get('routing').toJS();
}
});

Nuestro selector entonces se encarga de obtener los datos desde la propiedad routing (o la que decidamos usar) y convertirlos a objetos planos de JS para que los pueda usar react-router-redux sin problemas y de forma completamente transparente.

Conclusión

Usar Immutable.js nos permite trabajar con un estado verdaderamente inmutable evitando problemas comunes como pueden ser mutar directamente una propiedad sin crear una copia del estado lo cual puede causar errores de inconsistencia de datos y dolores de cabeza a muchos desarrolladores.

Además que Immutable.js es bastante fácil de usar por lo que incluso nos facilita nuestro trabajo como desarrolladores enormemente, por lo que vale mucho la pena empezar a usarlo.