Walrus Icon

The Coding Walrus

Guía Rápida de Redux

Guía Rápida de Redux

Esta guia tiene mucho mas sentido si ya conoces Redux y necesitas refrescar un poco, o si ya leiste Conceptos de Redux. Asumiremos que sabes que es el store, los reducers, las acciones, y los selectores y solo vamos a ver como es que se escriben y se aplican en el código. Una de las criticas principales de Redux es que es verboso y que tiene mucho boilerplate code que lo hace repetitivo, lo cual no es medianamente grave considerando todo lo que podemos hacer con Redux. Como estamos separando los intereses, vamos a movernos entre varios archivos y es importante conocer la responsabilidad de cada uno de estos y como debería estar estructurado el código.

Vamos a movernos de lo que modificaremos mas comúnmente a los archivos que tocamos menos, como los de configuración.

Tabla Rápida

La mejor forma de usar esta guía es que sepas que es lo que buscas hacer. Teniendo eso, buscas a que archivo le corresponde esa responsabilidad y ahí puedes mirar como es que se escribe para adaptarlo a tu caso de uso.

Que quieres hacer Sección
Conectar un componente al store y acciones Connect
Cambiar estado en el store Acciones, Reducers
Pasar una porción del estado a un componente Selectors
Modificar el estado para un componente Selectors
Derivar valores del estado Múltiples selectores
Iniciar Redux en un proyecto Provider, Store, Root Reducer

Debugging en Redux

Los problemas relacionados directamente a Redux están en uno de 3 lugares: el store, los reducers, o las acciones. Con redux-logger cada acción emite un log del store previo, la acción con su type y payload, y el store después de la acción. Si el problema es en Redux, lo podrás encontrar con facilidad en ese log:

  • Si no se emite un log no se esta disparando la acción y el problema radica en el componente o connect.
  • Si se emite y el type de acción o payload están mal el problema esta en la acción.
  • Si la acción esta bien pero el store resultante no tiene los valores esperados el problema esta en los reducers.
  • Si no hay error en el log el problema esta en los selectores.
Donde esta el problema Donde mirar
Los datos del store no están como deberían Root Reducer, Reducers
Una acción no hace nada Connect, el Componente
Una acción no hace lo que esperas Reducers, Acciones
Los props no llegan como quiero a mi componente Selectors, Múltiples selectores

Archivos de aplicación

Estos son los archivos que mas vamos a mover en Redux y se encargan de estructurar, modificar, y seleccionar el estado. Para saltar a los archivos requeridos para cuadrar Redux inicialmente, vallan a Archivos de configuración. Si ya tienen cuadrado Redux y necesitan conectarlo al store y las acciones vallan a Connect.

Acciones

Estos archivos crean las funciones que vamos a pasar a nuestros reducers para modificar el estado.

Responsabilidades de las Acciones:

  • Emitir acciones estructuradas para nuestros reducers.
  • Mantener consistencia en nuestros tipos de acciones para el estado.
// redux/actions/cart.actions.js
export const cartActionTypes = {
  ADD_ITEM: 'ADD_ITEM',
  REMOVE_ITEM: 'REMOVE_ITEM',
  CLEAR_CART: 'CLEAR_CART',
}

export conts addItemAction = item => ({
  type: cartActionTypes.ADD_ITEM,
  payload: item,
})

export const removeItemAction = item => ({
   type: cartActionTypes.REMOVE_ITEM,
   payload: item,
});

export const clearCartAction = () => ({
   type: cartActionTypes.CLEAR_CART,
});

Anatomía de una acción

interface Action {
  type: string;
  payload?: any;
}

Una acción siempre tiene un type, que el reducer usa para interceptar la acción. El payload es opcional si nuestra acción requiere de datos para los cambios que queremos en el estado.

Objeto de tipo de acciones

Como vamos a utilizar estos tipos en acciones y en el reducer, es preferible manejarlos desde un objeto para no equivocarnos pasando strings manualmente y para que en el caso de que cambiemos los tipos lo podamos hacer fácilmente desde este objeto.

Acciones

Las acciones son funciones con un tipo establecido que retornan un objeto de acción cuando se invocan. No manejan lógica simplemente comunican el tipo de acción y, opcionalmente, envían un paquete de datos para ser procesados.

Reducers

Estos archivos se encargan de la lógica para cambiar partes de nuestro store y son lo que se pasa a nuestro root-reducer para crear nuestro store. Este estado NO PUEDE SER MODIFICADO DIRECTAMENTE ya que no generara un cambio de estado que se detecte. Los cambios se deben hacer usando copias superficiales o usando objetos nuevos.

Responsabilidades de los Reducers:

  • Interceptar acciones.
  • Modificar el estado de acuerdo a la acción emitida.
  • Dejar pasar el estado si no hay una acción relevante a el reducer.
// redux/reducers/cart.reducer.js
import { cartActionTypes } from '../actions/cart.actions.js';

const INITIAL_STATE = {
  hidden: true,
  cartItems: [],
};

const cartReducer = (state = INITIAL_STATE, action) => {
  const { type, payload } = action;
  switch (type) {
    case cartActionTypes.ADD_ITEM:
      return {
        ...state,
        cartItems: [...state.cartItems, payload],
      };
    case cartActionTypes.REMOVE_ITEM:
      return {
        ...state,
        cartItems: state.cartItems.filter(item => item !== payload),
      };
    case cartActionTypes.CLEAR_CART:
      return {
        ...state,
        cartItems: [],
      };
    default:
      return state;
  }
};

export default cartReducer;

Reducer

El reducer toma dos argumentos, el estado, y una accion. De la acción siempre viene type y frecuentemente payload. En el reducer el state es la parte que le corresponde a este reducer, no el state completo. Acá estamos destructurando dentro del reducer, pero se puede hacer en los parámetros de la función. Vale notar que todas las acciones pasan por todos los reducers.

Caso default

Siempre tenemos que tener un caso default que retorna el estado actual para que cuando se dispare una acción que no se procese, el estado sea pasado al store sin cambios.

Caso con payload

Se intercepta la acción y se generan cambios en el estado que utilizan los valores que se pasan en el payload para procesar. El payload puede ser lo que quieran, un indice para filtrar, un valor, etc. Las modificaciones al estado se deben hacer con valores nuevos, es decir con estilo inmutable. En este caso preservamos el valor de hidden en todas las modificaciones, y alteramos cart items con métodos que retornan arrays nuevos.

Caso sin payload

En nuestro reducer, la acción CLEAR_CART no tiene payload porque va a quitar todos los artículos de cartItems. Hay estos casos y otros que se pueden manejar sin necesidad de un payload.

Selectors

Los selectors nos permiten tomar porciones del estado sin tener que crear un estado nuevo en el store para la selección y derivar valores nuevos de este estado. Con los selectores manejamos la lógica que se haría dentro de un componente en especifico para pasarle el prop modificado, separando la parte de lógica de la presentacional. En este caso estaremos usando el paquete reselect que memoiza los selectores, mejorando el desempeño.

Responsabilidades de los Selectors:

  • Seleccionar sub-porciones de el store.
  • Modificar porciones del estado de nuestros reducers rara casos específicos de componentes.
  • Encargarse de la lógica local de un componente que requiere algún valor del store.
  • Derivar estado de el store usando varias porciones del store u otros valores.
// redux/selectors/cart.selectors.js
import { createSelector } from 'reselect';

const selectCart = state => state.cart;

export const selectCartItems = createSelector(
  [selectCart],
  cart => cart.cartItems
);

createSelector

Create selector nos crea un selector con cache que toma parte del estado. Como primer argumento toma un Array de selectores y los pasa al segundo argumento que es un callback al que se le pasan las porciones del estado. En este caso tomamos todo el estado de cart, y retornamos cartItems sin hidden.

Múltiples selectores

import { createSelector } from 'reselect';

const selectStoreItems = state => state.storeItems;
const selectFilterValues = state => state.filterValues;

export const selectItemsInStock = createSelector(
  [selectStoreItems],
  storeItems => storeItems.filter(item => item.inStock === true)
);

export const selectItemTotal = createSelector([selectItemsInStock], items =>
  items.reduce((total, item) => total + item.price, 0)
);

export const selectFilteredItems = createSelector(
  [selectItemsInStock, selectFilterValues],
  (items, filterValues) => {
    const { maxPrice, minPrice } = filterValues;
    return items.filter(item => item.price < maxPrice && item.price < minPrice);
  }
);

En este caso demostramos todo lo que se puede hacer con selectores:

  • Seleccionamos storeItems y filterValues del store.
  • Seleccionamos los artículos que están en stock filtrando por el boolean inStock.
  • Usamos el selector anterior para sacar el precio total de los artículos en stock usando reduce, lo que remuevo la necesidad de guardar este valor en el store porque lo derivamos de los datos principales.
  • Usamos el selector de stock y de los filtros para derivar un estado de nuestros items que usa los valores de los filtros para retornar los items procesados para presentar en nuestro componente.

Podemos pasar cualquier selector a createSelector para tener selectores que se van componiendo con selectores mas pequeños, permitiéndonos re-utilizar la mayor cantidad de lógica posible.

Connect

La función connect es la que le da acceso a nuestros componentes a el store y las acciones para consumir estado y para modificarlo.

Responsabilidades de connect:

  • Darle acceso al store a los componentes.
  • Permitir despachar acciones en nuestros componentes.
  • Pasar estos valores como props a cualquier componente.
// components/cart.component.jsx
import React from 'react';
import { connect } from 'react-redux';
import { selectCartItems } from '../../redux/selectors/cart.selectors.js';
import {
  addItemAction,
  removeItemAction,
  clearCartAction,
} from '../../redux/actions/cart.actions.js';

import { CartContainer } from './cart.styles.js';

const Cart = ({ cartItems, addItem, removeItem, clearCart }) => (
  <CartContainer>cartItems.map(item => ( ... ))</CartContainer>
);

const mapStateToProps = state => ({
  cartItems: selectCartItems(state),
});

const mapDispatchToProps = dispatch => ({
  addItem: item => dispatch(addItemAction(item)),
  removeItem: item => dispatch(removeItemAction(item)),
  clearCart: () => dispatch(clearCartAction()),
});

export default connect(mapStateToProps, mapDispatchToProps)(Cart);

connect, mapStateToProps, mapDispatchToProps

Connect es una función currificada que toma el estado que vamos a pasarle al componente y las acciones que vamos a pasarle al componente. Como convención se usan esos nombres.

mapStateToProps

Es una función que toma como argumento el estado y retorna piezas de el. Nosotros estamos usando selectores a los cuales les pasamos el estado y nos traen la porción del estado que seleccionamos con nuestra lógica. Retorna un objeto con los props, y sus nombres, que se van a enviar a nuestro componente.

mapStateToProps

Esta función toma como argumenta una función dispatch que es la que se encarga de despachar las acciones a los reducers. Retornan un objeto con los nombres y las funciones que vamos a pasarle a nuestro componente. Si la acción espera un payload, hay que pasarlo en mapDispatch to props.

connect de solo state o dispatch

Si solo necesitamos estado se usaría connect de la siguiente manera:

export default connect(mapStateToProps)(Cart);

Si solo necesitamos despachar una acción lo haríamos de la siguiente manera:

export default connect(null, mapDispatchToProps)(Cart);

Los pasos son:

  1. Importar connect, los selectores, y las acciones.
  2. Crear mapStateToProps y/o mapDispatchToProps.
  3. Conectar estas funciones a nuestro componente usando connect.
  4. Destructurar o utilizar props en nuestro componente con los nombres de mapState/dispatchToProps.

Como se puede ver en este ejemplo, no necesitamos usar return o hooks porque todo el estado lo reemplazamos con Redux y la lógica de procesar los props la manejamos con selectores.

Uso con react-router y mas librerías HOC

Como ambas librerías usan el patrón de higher order component (HOC) hay que envolver nuestro componente en ambas funciones:

export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Cart));

Estas funciones se evalúan de adentro hacia afuera, conectamos nuestro componente a Redux, y luego este componente conectado se pasa a withRouter. Para que se vea mas limpio y si estamos usando mas librerías que utilizan el patrón HOC, podemos usar una función de compose o pipe para crear la cadena:

// con compose (Más comun)
export default compose(
  withRouter,
  connect(mapStateToProps, mapDispatchToProps)
)(Cart);
// con pipe
export default pipe(
  connect(mapStateToProps, mapDispatchToProps),
  withRouter
)(Cart);

Compose evalúa las funciones de derecha a izquierda, pipe las evalua de izquierda a derecha. Ambas se encuentran en librerías como Ramda, Lodash, Underscore; Redux trae compose en el paquete. Estas funciones son útiles para entender mejor lo que se esta haciendo cunado hay 3 o mas librerías que vamos a usar para mejorar/enhance nuestro componente.

Archivos de configuración

Estos son los archivos que menos van a tocar y solo hay uno de cada uno. Son para el manejo general de Redux y mutan mucho dependiendo de las integraciones que quieras manejar en tu app. Para saltar a los archivos mas comunes de Redux, vallan a Archivos de aplicación. Si ya tienen cuadrado Redux y necesitan conectarlo al store y las acciones vallan a Connect.

Provider

Este componente es el que va a recibir el store y darle acceso a cualquier componente dentro de el a el estado del store.

Responsabilidades del Provider

  • Darle acceso al store a nuestra aplicación.
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';

import App from './App';
import { store } from './redux/store';

ReactDOM.render(
  <Provider store={store}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>,
  document.getElementById('root')
);

Provider

Colocamos el Componente Provider sobre nuestro App y le pasamos el store como el prop store para que nuestros componentes puedan usarlo.

En este ejemplo utilizamos BrowserRouter para ilustrar como seria usando un Router popular como React-Router.

Store

En este archivo vive el estado read-only y es un archivo que no se cambia a menudo.

Responsabilidades del Store

  • Crear el Store
  • Aplicar el Middleware que queremos utilizar
// redux/store.js
import { createStore, applyMiddleware } from 'redux';
import logger from 'redux-logger';
import { rootReducer } from './root-reducer';

const middleware = [];

if (process.env.NODE_ENV === 'development') middleware.push(logger);

export const store = createStore(
  persistRootReducer,
  applyMiddleware(...middleware)
);

createStore

createStore crea el store que vamos a utilizar en nuestra aplicación. Toma como primer argumento el rootReducer que es el reducer que combina los reducers de cada pieza de estado. El segundo argumento puede ser un estado inicial o un enhancer que le da funcionalidad adicional a nuestro store con middleware. Si se pasa un estado inicial, el enhancer seria el tercer argumento.

applyMiddleware

Toma una lista de middleware y se lo aplica al store. En este ejemplo usamos un middleware de desarrollo que no queremos que este en producción, por lo cual lo inyectamos en desarrollo. En caso de usar middleware de producción, lo pasaríamos directamente en nuestra const middleware.

Root Reducer

Este archivo une todos nuestros reducers y es el que se encarga de modificar el store.

Responsabilidades del Root Reducer

  • Unir Reducers.
  • Nombrar las propiedades del store.
// redux/root-reducer.js
import { combineReducers } from 'redux';

import userReducer from './reducers/user.reducer';
import cartReducer from './reducers/cart.reducer';
import directoryReducer from './reducers/directory.reducer';
import shopReducer from './reducers/shop.reducer';

export const rootReducer = combineReducers({
  user: userReducer,
  cart: cartReducer,
  directory: directoryReducer,
  shop: shopReducer,
});

combineReducers

Toma un objeto que determina como es nuestro store. Le pasamos los reducers que determinan la estructura de cada pieza del estado. En este caso, nuestro store tendrá las propiedades: user, cart, directory, y shop. Si no colocamos los nombres, se llamaran como el reducer que le pasamos al objeto.

Mejores Practicas

  • Mantener el estado del store lo mas normalizado/plano posible.
  • No meter estado que se puede derivar con selectores en el store. Si necesitas un total o unos valores filtrados, hazlo derivando valores con selectores para evitar agregarle peso innecesario al store y que nuestras actualizaciones de estado sean mas eficientes.
  • Nombrar los archivos nombre.tipo.js para que sepamos si estamos trabajando con actions, reducers, o selectors fácilmente cuando tenemos varios archivos abiertos en nuestro IDE.
  • Usar Connect sobre hooks: Redux tiene un api para usar hooks, sin embargo connect funciona mejor en desempeno, causa muchas menos renderizaciones y extrae el manejo de lógica dentro de nuestro componente mas directamente. Redux Connect vs Hooks
  • Crear el objeto de tipos de acciones para evitar errores ortográficos entre acciones y reducers.
  • Nombrar las propiedades de nuestros objetos de acciones type y payload.
  • Usar middleware de desarrollo (Solo en desarrollo) para ver es store y las acciones que son los dos puntos donde pueden haber errores de interface usando redux. Redux-logger
  • Nombrar acciones con <nombreDeAccion>Action para que no se crucen con posibles nombres en props del componente:
import { clearCart } from '../../redux/actions/cart.actions.js'
...
const Cart = ({ clearCart }) => (
  ...
);

const mapDispatchToProps = dispatch => ({
  clearCart: () => dispatch(clearCart()),
});

Si la acción se llamara clearCart tendría el mismo nombre del import por lo cual la podríamos accidentalmente usar dentro de nuestro componente sin activar los reducers porque no la pasamos por dispatch. Si no la agregamos a mapDispatchToProps no tendríamos un error en el IDE y no funcionaria nuestro Redux. Es una practica que mejora mucho la calidad de vida.

Conclusión

Redux se puede manejar de muchas formas. Es importante conocer el código mas común que veremos usando Redux y cuales son sus responsabilidades para mantener la separación de intereses y tener nuestro código mas limpio y estructurado. En ultimas son varias piezas sencillas que se conectan, y el boilerplate objetivamente no es nada complejo ni largo. Con esta guía, sabiendo que tienes que hacer con tus componentes, es solo que busques a que archivo le corresponde la responsabilidad que necesitas y mirar como es que se escribe para adaptarlo a tu caso de uso. Cubrimos acciones, reducers, selectores, connect, provider, store, root-reducer, y mejores practicas para sacarle provecho a Redux y estandarizar su uso en nuestras aplicaciones. Si disfrutaste esta guía y te gustaría conocer mas de la teoría detrás de Redux lee Conceptos de Redux .