Redux es una librería que se encarga de emitir actualizaciones de estado en respuesta a acciones. En lugar de modificar directamente el estado, la modificación se maneja a través de objetos sencillos llamados acciones. Luego escribes una función especial llamada reductor para decidir cómo cada acción transforma el estado de toda la aplicación. Y por último todo se maneja desde un solo lugar llamado store (ya somos experto en Redux! No?). Ya que la repetición nos ayuda a recordar las cosas podemos decir que los elementos principales de esta arquitectura o patron de diseño son:
- Acciones
- Reducers
- Store
La librería Redux en sí es sólo un conjunto de ayudas para «montar» reductores a un único objeto de store global. Veamos los detalles.
Ilustración por Lynn Fisher
Aunque no sea importante para entender qué es Redux, debemos indicar que para instalar la versión estable usamos npm:
npm install --save redux
Principios fundamentales
Como los requisitos para las aplicaciones tipo SPA en JavaScript se han complicado cada vez más, nuestro código debe gestionar más states que nunca. Las librerías como React, Angular y Vuejs intentan resolver parte de este problema en el layer de vista eliminando tanto la asincronía como la manipulación DOM directa. Sin embargo, la gestión del state de la data se deja a tu discreción. Aquí es donde Redux entra.
Redux intenta hacer predecible las mutaciones de estado imponiendo ciertas restricciones sobre cómo y cuándo pueden suceder las actualizaciones. Estas restricciones se reflejan en los tres principios de Redux.
Única fuente de verdad
El estado de toda la aplicación se almacena en una estructura de árbol dentro de un único store.
Esto hace que sea fácil crear aplicaciones universales, ya que el estado en el servidor puede ser serializado e hidratado en el cliente sin ningún esfuerzo adicional de codificación. Un solo árbol de estado también facilita la depuración o la introspección de una aplicación. Esto permite que el ciclo de desarrollo sea más rápido.
El estado es read-only
La única manera de cambiar el estado es emitir una acción, un objeto que describe lo sucedido.
Esto garantiza que nada modificará directamente al estado de tu aplicación. Por el contrario, la acción expresa una intención de transformar el estado. Debido a que todos los cambios están centralizados y suceden uno por uno en un orden estricto, no hay race conditions que necesitemos monitorear. Dado que las acciones son objetos sencillos, se pueden registrar, serializar, almacenar y reproducir posteriormente para fines de depuración o pruebas.
Los cambios se realizan a través de funciones puras
Para especificar cómo se transforma el estado por acciones, se escriben reducers puros.
Los reducers son funciones puras que toman el estado anterior y una acción, y devuelven el siguiente estado. Esta función debe devolver objetos que presentan el nuevo estado, en lugar de cambiar el estado anterior. Puedes comenzar con un solo reductor y, a medida que la aplicación vaya creciendo, divídelo en reductores más pequeños que administren partes específicas del estado.
Actions
Las acciones son cargas útiles de información que envían datos desde su aplicación a su store. Son la única fuente de información para el store. Los envía al store mediante el método store.dispatch ()
.
Las acciones son simples objetos en JavaScript. Las acciones deben tener una propiedad conocida como type
que indica el tipo de acción que se está realizando. Los types
normalmente se deben definir como una cadena de constantes. Una vez la aplicación haya crecido, puedes mover las acciones a módulos independientes. Veamos un ejemplo:
var action = {
type: 'ADD_USER',
user: {name: 'Dan'}
};
Action Creators
Los creadores de acciones son exactamente eso, funciones que crean acciones. Es fácil combinar los términos «acción» y «creador de acción» al momento de buscarle nombres a nuestros elementos, así que haz lo mejor que puedas para usar el término apropiado. Así evitaras confusiones.
// Action type
const ADD_USER = 'ADD_USER';
// Action Creator
var addUser = function(name) {
return { type: ADD_USER, name}
}
// Dispatch envía un object
store.dispatch(addUser('Jaime'));
La función dispatch()
se puede accesar directamente desde el store como store.dispatch()
, pero es más conveniente utilizar el método connect()
que tenemos en la librearía react-redux. Puedes utilizar bindActionCreators()
para asociar automáticamente cuantos creadores de acción desees a la función dispatch()
. Ya empezamos a tocar un poco de profundidad asi que mejor vamos a continuar con el próximo tema.
Reducers
Las acciones describen el hecho de que algo ocurrió, pero no especifican cómo cambia el estado de la aplicación en respuesta a esa intención de cambio. Este es el trabajo de los reductores. El reductor es una función pura que toma el estado anterior y una acción, y devuelve el siguiente estado.
(previousState, action) => newState
Es muy importante que el reductor se mantenga puro. Cosas que nunca debes hacer dentro de un reductor:
- Modificar sus argumentos (entonces ya no es una función pura)
- Ejecutar acciones como llamadas a algún API o jugar con las rutas (routes)
- Ejecutar funciones no-puras, por ejemplo Date.now(), Math.random(), etc
La documentación es enfática en destacar que el reductor debe ser puro. Dado los mismos argumentos, debes calcular el siguiente estado y devolverlo. Sin sorpresas. Sin efectos secundarios. No llamadas de API. No mutaciones. Sólo una simple evaluación. Veamos un ejemplo:
import { combineReducers } from 'redux'
import { ADD_USER } from './actions'
function users(state = [], action) {
switch (action.type) {
case ADD_USER:
return [
...state,
{
text: action.text
}
]
default:
return state
}
}
const reducers = combineReducers({
users
})
export default reducers
Store
Como hemos visto las acciones representan los hechos sobre «lo que pasó» y los reductores actualizan el estado de acuerdo a esas acciones. El store es el objeto que los reúne (yey!). El objeto store tiene las siguientes responsabilidades:
- Almacena el estado de aplicación
- Permite el acceso al estado a través del método
getState()
- Permite que el estado se actualice a través del método
dispatch(action)
- Registra los listeners mediante el método
subscribe(listener)
- Maneja remover el registro de los listeners a través de la función que invocó al
subscribe(listener)
. Te lo aclaro con el siguiente ejemplo:
const remove = subscribe(myListener);
// para remover:
remove();
Ya cubierto lo básico podemos tener una idea de lo que es Redux. Veamos el ejemplo completo (en producción se debe perseguir crear módulos para cada parte):
// Reducers
const users = (state = [], action) => {
switch (action.type) {
case 'ADD_USER':
return [
...state, {
text: action.text
}
]
default:
return state
}
}
//Actions
const addUser = (text) => {
return {
type: 'ADD_USER',
text
}
}
// Redux y amigos
const { combineReducers, createStore } = Redux
const userApp = combineReducers({ users })
let store = createStore(userApp)
// Registra el estado inicial
console.log(store.getState())
// cada vez que haya un cambio en el state, registrarlo
// Observa que la función subscribe() devuelve una función para remover el listener
let unsubscribe = store.subscribe(() =>
console.log(store.getState())
)
// Dispatch some actions
store.dispatch(addUser('Wolverine'))
store.dispatch(addUser('Batman'))
store.dispatch(addUser('Ironman'))
// Remover listeners a la actualización del state
unsubscribe()
Conclusión
Esto es solo el comienzo. Esta arquitectura puede parecer un poco pesada para proyectos pequeños, pero la belleza de este patrón de diseño es lo bien que escala en aplicaciones grandes y complejas. Además, es fácil de integrar en ya sea en React, Angular, Vuejs etc.