Redux

Una de las mayores problemáticas de React es la forma en que se administran los estados y como los cambios pueden afectar a otros componentes. Como ya hemos venido viendo, React propaga los cambios entre sus componentes de arriba-abajo y de abajo-arriba, es decir, un componente solo se puede comunicar con sus hijos directos y con su padre.

React change state
React change state

Para comprender este problema, analicemos la imagen anterior, e maginemos que un cambio producido en el componente J, deberá reflejarce en el componente G. Primero que nada, nos queda claro que no es posible realizar una actualización directa, pues J y G no son decendientes directos, por lo que la única forma de hacer llegar el cambio hasta G, es propagar los cambios hacia arriba, hasta encontrar el primer pariente en común y luego replicar los cambios hacia abajo, hasta llegar al componete G.

Para lograr que un cambio se propague de esta forma, es necesario que todos los componentes involucrados en la cadena, conozcan la propiedad y las repliquen a sus descendientes, sin embargo, pasar propiedades solo aplica para replicar los cambios de arriba abajo, y para replicar los cambios de abajo arriba, es necesario que los padres maden funciones como props para que los hijos las ejecuten para notificar al padre de los cambios.

Este problema se puede repetir varias veces en una aplicación, sobre todo en aquellas páginas muy complejas donde hay una gran cantidad de elementos, lo que puede convertirse rápidamente un problema. Ya que administrar props y funciones por toda la estructuractura crea componentes sumamente complejos y difíciles de entender y mantender.

Introducción a Redux

Redux es una herramienta que nos ayuda a gestionar la forma en que accedemos y actualizamos el estado de la aplicación de una forma centralizada y controlada. Mediante Redux es posible centralizar el estado general de la aplicación en algo llamado store, liberando a los componentes la responsabilidad de administrar un estado interno.

Cambio del estado con Redux.
Cambio del estado con Redux.

Redux funciona prácticamente igual que el Context, pues permite gestionar el estado desde una estructura externa a la jerarquía de componentes, aunque tiene una diferencia importante, el Context es utilizado para guardar datos globales, es decir, que son de interés para toda la aplicación, mientras que Redux, permite gestionar datos comúnes que son relevamente para una página determinada.

Este último pude resultar confuso, por que no queda claro cuando algo de global y cuando no, sin embargo, hay una forma de saberlo, si un determinado dato es relevante para toda la aplicación, entonces se considera global, por otro lado, si un determinado dato, solo es relevante solo para una determinada página, entonces es un dato que podríamos gestionar con Redux.

Como funciona Redux

Antes que nada, es importante aclarar que Redux es una librería que nace para gestionar el estado de cualquier aplicación de una sola página (SPA) basada en JavaScript, por lo que posible utilizarla con React, JQuery, Angular o una simple página que utiliza JavaScript puro. Dicho lo anterior, pasemos a explicar como funciona Redux.

Lo primero que debemos aprender son los componentes que conforma Redux y como estos encajan para administrar el estado de la aplicación:

  • Store: Representa el estado de la aplicación, es conocido dentro de Redux como “la única fuente de la verdad”.
  • Reducers: Son funciones JavaScript puras que determinan como deberá ser actualizado el store en función de las acciones (actions).
  • Actions: Son objetos JavaScript que describen una intención para cambiar el estado del store.
  • Dispatch: Es una función que permite lanzar acciones (actions) al store, con la intención de afectar el estado.

Ahora bien, de nada sirve listar los componentes que conforman Redux sin no entendemos como interactúan, por lo que vamos a ver con una serie de imágenes, cual es el procesos desde el cual se crea el store, se lanza una acción, se actualiza el estado con los reducers y finalmente, los cambios son propagados por los componentes.

Paso 1: Creando el store

El primer paso para utilizar Redux es crear el store, el cual dijimos que es un objeto independiente a la estructura de la jerarquía de componentes, el cual está totalmente desconectado de los componentes al momento de su creación:

Creamos el store
Creamos el store

Para crear el store, Redux nos proporciona la función createStore, el cual recibe como parámetro un reducer, o un conjunto de estos. Si nuestro store solo esta compuesto de un reducer, lo podemos pasar directamente como parámetro la función createStore, sin embargo, la mayoría de las veces, es necesario utilizar más de un reducer, por lo que utilizamos la función combineReducer para agruparlos.

1import { createStore, combineReducers } from 'redux'  
2import reducer1 from '../redux/reducers/reducer1'  
3import reducer2 from '../redux/reducers/reducer2'  
4import reducer3 from '../redux/reducers/reducer3'  
5    
6const store = createStore(  
7    combineReducers({  
8        reducer1,  
9        reducer2,  
10        reducer3  
11    })  
12)  
13    
14export default store  

Observa que para crear el store estamos utilizando 3 reducers, los cuales analizaremos más adelante.

Paso 2: Registrandonos al store

El siguiente paso es hacer que nuestros componentes se registren al store, con la intención de que reciban las actualizaciones en caso de alguien actualice el store.

Para esto, es necesesario realizar dos pasos, el primero: crear un componente Provider que deberá englobar toda la aplicación.

1import { Provider } from 'react-redux'  
2import store from './redux/store'  
3    
4    
5const MyApp = (props) => {  
6    
7    return (  
8    <Provider store={store} >  
9        <MyComponent />  
10    </Provider>  
11    )  
12}  
13export default TwitterApp  

El componente Provider es provisto por la librería react-redux, la cual es una librería que hace un wrapper de la librería redux para facilitar su uso con React. Provider recibe como parámetro el store, el cual creamos en el paso anterior.

El Provider es importante por que es el componente al cual se registran los componentes para recibir las actualizaciones cuando el store cambie, es por este motivo que cualquier componente que quiera registrarce al store, tendrá que ser descendiente de Provider.

El segundo paso es registrar registrar nuestros componentes para recibir las actualizaciones del store, para esto, utilizamos el hook useSelector en todos los componentes donde queremos sincronizar con el store.

1import { useSelector } from 'react-redux'  
2    
3    const MyComponent = (props) => {  
4        
5        const state = useSelector(state => state.myState)  
6        
7        return (  
8        <MyComponent>  
9            <p>{state.name}</p>  
10        </MyComponent/>  
11        )  
12        
13    }  
14    export default MyComponent  

Para conectar un componente al store, tenemos el hook useSelector, el cual recibe como parámetro una función, dicha función recibirá como parámetro el estado general del store, y la función deberá de retornar la sección del store que nos interesa, de esta forma, cada vez que el store cambie, el hook actualizará el componente con los nuevos valores, lo que detonará un nuevo renderizado del componente para reflejar los nuevos valores.

Conectamos los componetes al Store
Conectamos los componentes al Store

En este punto, la aplicación se verá como en la imagen anterior, donde los componentes interesados en recibir actualizaciones se registraran al store por medio del componente Provider. Utilizaremos los componentes en color purpura para identificar los componentes que están registrados al store.

Paso 3: Despachando acciones

Una vez que los componentes se registran al store, podrán despachar (dispatcher) acciones (actions), con la intenación de manifestar una intención de actualizar el estado del store.

Para despachar una acción son necesarios dos cosas, un referencia al dispatcher y el objeto action que describe los cambios a realizar en el store. Para recuperar el dispatcher contamos con el hook useDispatch:

1import { useDispatch, useSelector } from 'react-redux'  
2    
3    const MyComponent = (props) => {  
4        
5        const dispatch = useDispatch()  
6        
7        const state = useSelector(state => state.myState)  
8    
9        return (  
10        <MyComponent>  
11            <p>{state.name}</p>  
12        </MyComponent/>  
13        )  
14        
15    }  
16    export default MyComponent  

El siguiente paso es despachar acciones para actulizar el estado:

1import { useDispatch, useSelector } from 'react-redux'  
2    
3    const MyComponent = (props) => {  
4        
5        const dispatch = useDispatch()  
6        
7        const state = useSelector(state => state.myState)  
8        
9        const dispatchAction = () => {  
10        dispatch({  
11            type: "ACTION_NAME",  
12            value: {...}  
13        })  
14        }  
15        
16        return (  
17        <MyComponent>  
18            <p>{state.name}</p>  
19            <button onClick={dispatchAction} >Update</button>  
20        </MyComponent/>  
21        )  
22        
23    }  
24    export default MyComponent  

Para despachar una acción, es necesario enviar un objeto que tenga al menos la propiedad type, la cual es utilizada por los reducers para identificar el cambio que se quiere realizar, además, es posible enviar cualquier otra propiedad para complementar la acción, el cual puede tener los nuevos datos para el store.

Despachando una acción
Despachando una acción

En este punto, podemos ver que el componente I ha despachado una acción con la intención de actualizar el estado.

tip

Importante

Los actions deben de tener al menos la propiedad type, ya que el valor de esta propiedad le indica al reducer que como debe de actualizar el estado.

Tambien es importante resaltar que el action solo es una intención para cambiar el estado, por lo que no hay garantía de que lo actualice, sino que es el reducer el que determina si deberá hacer algo con el action.

Paso 4: Procesar los actions con los reducers

Una vez que un action es despachado, el store lo recibirá y lo enviará a los reducers, los cuales, basado en la propiedad type, determinarán, si es necesario modificar el estado.

Procesando las acciones con los Stores
Procesando las acciones con los Stores

Si bien solo puede existir un solo Store con un solo estado, el Store puede tener varios reducers que modifican el estado. Un reducer es básicamente una función JavaScript pura, que recibe como entrada el estado actual de la aplicación y el Action que describe el intento por actualizar el estado, de esta forma, es el reducer quien determina como deberá ser actualizado el estado basado en el action.

Algo importante a mencionar es que el estado se estructura basado en los reducers, de tal forma que, cada reducer modifica una sección del estado, es por ello que un mismo action es recibido por todos los reducers y cada uno de ellos podrá determinar si es necesario hacer algún cambio a su parte del estado, por lo tanto, es posible que un solo action tenga efectos en varias partes del estado. Pero veamos un ejemplo de un reducer para entender de que estamos hablando:

1const initialState = {  
2    values: []  
3}  
4    
5export const myReducer = (state = initialState, action) => {  
6    switch (action.type) {  
7        case "INIT":  
8            return {  
9                values: action.value  
10            }  
11        case "CREAR":  
12            return initialState  
13        default:  
14            return state  
15    }  
16}  
17export default myReducer  

Si observas el reducer anterior, te podrás dar cuenta que se trata de función JavaScript como y corriente, la cual recibe como entrada el state actual y el action, también podrás ver que el state es igualado a la constante initialValue, lo que significa que declaramos el estado inicial del store.

Dentro del cuerpo del reduce podemos ver un switch utilizado para realizar una acción diferente basado en el valor de la propiedad type del action. Si el type del action corresponde con alguno de los cases, entonces procederemos ha aplicar algún cambio en el estado del estore, de esta forma, el valor que retornemos será el que se guardará en el nuevo estado del store, pero si por otra parte, el type no corresponde con ninguna acción, entonces simplemente retornamos el mismo estado que recibimos para indicarle al store que no se ha aplicando ningún cambio.

Actualizando el estado
Actualizando el estado

Un punto que no hemos abordado es que, la estructura del estado dentro del reducer es determianda por los reducers, de esta forma, un store tiene una forma de árbol, donde cada reducer crea una rama, por ejemplo, si tenermos 3 reducers, estos crearían 3 ramas con el nombre establecidos al momento de crear el store. Veamos un ejemplo de como se crear el store:

1import { createStore, combineReducers } from 'redux'  
2import tweetsReducer from '../redux/reducers/tweetsReducer'  
3import userReducer from '../redux/reducers/userReducer'  
4import configReducer from '../redux/reducers/configReducer'  
5    
6const store = createStore(  
7    combineReducers({  
8        tweets: tweetsReducer,  
9        user: userReducer,  
10        config: configReducer  
11    })  
12)  
13    
14export default store  

Este store dará como resultado un estado con la siguiente estructura:

1{  
2    tweets: {  
3        ...  
4    },    
5    user: {  
6        ...  
7    },    
8    config: {  
9        ...  
10    }     
11}  

Si observar, el nombre de las propiedades que tiene el estado, corresponde con el nombre con el que creamos el store, por otra parte, la estructura interna de cada sección será determinado por el valor que retorne cada reducer, por lo tanto, el tweetsReducer solo podrá modificar la sección tweets, userReducer solo podrá modicar la sección user y configReducer solo podrá modificar la sección config. Entonces podemos decir que cada reducer solo puede modificar una subparte del estado general.

Paso 5: Replicando los cambios a los componentes

Finalmente, cuando los reducer producen el siguiente estado, el store es actualizado con el nuevo estado y todos los componentes previamente suscritos al store por medio del componente Provider será notificado de los cambios.

1import { useSelector } from 'react-redux'  
2    
3    const MyComponent = (props) => {  
4        
5        const state = useSelector(state => state.myState)  
6        
7        return (  
8        <MyComponent>  
9            <p>{state.name}</p>  
10        </MyComponent/>  
11        )  
12        
13    }  
14    export default MyComponent  

Recordemos que la suscripción al store se hace mediante el hook useSelector, por lo tanto, cuando el store se actualizado, el hook se actualizará ejecutando la función definida y el nuevos estado será retornado y asignado a la variable definida. Al mismo tiempo, el componente será nuevamente actualizado para reflegar los nuevos cambios.

Actualizando los componentes con el nuevo estado
Actualizando los componentes con el nuevo estado

Finalmente, cabe mencionar que durante todo el tiempo de vida de la aplicación, cualquier componente podrá despachar acciones, lo que implicaría que los pasos 3, 4 y 5 se podrían dar decenas, cientos o miles de veces, todo depende de que con que frecuencia la aplicación cambie.

Los tres principios de Redux

Adicional al ciclo de vida, es necesario conocer los 3 principios de Redux, los cuales se deberán cumplir siempre:

Principio 1: Una única fuente de la verdad

Redux funciona únicamente con un solo Store para toda la aplicación, es por eso que, se le conoce como la única fuente de la verdad. La estructura que manejemos dentro del Store dependerá totalmente de nosotros, por lo que somos libre de diseñarla a como se acomode mejor a nuestra aplicación, debido a esto, suele ser una estructura con varios niveles de anidación.

Principio 2: El estado es de solo lectura (read-only)

Una de las restricciones de Redux es que, no existe una forma para actualizar el estado directamente, en su lugar, es necesario enviar un action al Store, describiendo las intenciones de actualizar el estado.

Principio 3: Los cambios se realizan con funciones puras

Como ya lo habíamos mencionado, solo es posible actualizar el estado mediante un action, el cual manifiesta la intención de cambiar el estado al mismo tiempo que describe el cambio que quiere realizar. Cuando la acción llega al Store, este redirige la petición a los reducers.

Los reducers deberán ser siempre funciones puras. Para que una función sea pura, debe de cumplir con las siguientes características:

  1. No deberá de llamar a ningún recurso externo, como pueden ser bases de datos o servicios.
  2. Los valores retornados dependerán únicamente de los parámetros de entrada, lo que nos lleva al siguiente punto:
  3. los mismos parámetros de entrada, deberá dar siempre el mismo resultado.
  4. argumentos deben de considerarse inmutables, lo que implica que no podemos actualizarlos por ninguna razón.

Estos se llaman "puros" porque no hacen más que devolver un valor basado en sus parámetros. Además, no tienen efectos secundarios en ninguna otra parte del sistema.

Algo que probablemente no quedo muy claro con respecto al cuarto punto, es que, dado que el estado actual es un parámetro de entrada para el reducer, no deberíamos modificarlo, en su lugar, tendríamos que hacer una copia de él y sobre ese agregar los nuevos cambios.

Otro de las cosas a tomar en cuenta es que, los Action deben de tener una estructura mínima, en la cual debe de existir la propiedad type, seguido de esto, puede venir lo que sea, incluso, podríamos mandar solo la propiedad type, por ejemplo:

1//Option 1  
2{  
3    type: "LOGIN"  
4}  

1//Option 2  
2{  
3    type: "LOGIN",  
4    profile: {  
5    id: "1234",  
6    userName: "oscar",  
7    name: "oscar blancarte"  
8    }  
9}  

La propiedad type es importante, porque le dice a los reducers la acción que quieres realizar sobre el estado.

Redux sin hooks

Algo de lo que no hablamos es, como utilizar Redux sin hooks, lo cual puede ser importante si estamos dando mantenimiento a un proyecto más antiguo o que por alguna razón se decidio trabajar con componentes de clase.

Antes que nada, la forma de trabajar con clases es muy parecido a como si lo hiciéramos con hooks, con la única diferencia en que cambiamos la forma en que nos suscribimos al store y como despachamos las acciones, veamos un ejemplo:

1import { connect } from 'react-redux'  
2    
3    class MyComponent extends React.Component {  
4        render(){  
5            return {  
6                <div>  
7                    <p>{this.props.name}</p>
8                    <button onClick={this.props.myAction}>Click here!!</button>  
9                </div>  
10            }  
11        }  
12    }  
13        
14    const mapStateToProps = state => {  
15        return {  
16            name: state.user.name  
17        }  
18    }  
19        
20    const mapDispatchToProps = (dispatch, ownProps) => {  
21        return {  
22            myAction: () => {  
23                dispatch(setVisibilityFilter(ownProps.filter))  
24            }  
25        }  
26    } 
27export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)  

Lo primero que cambia es que ahora, tenemos que usar el fomoso infierno de los envoltorios para retornar nuetro componente envuelto en el componenten connect (línea 28). Connect es una función que retorna otra función, la cual a su vez, recibe un componte como parámetro y finalmente retorna un nuevo componente que envuelve a nuestro componente.

Connect recibe dos parámetros, mapStateToProps, la cual es una función que deberá indicar que secciones del estado del store deberán ser pasadas al componente como props y finalmente, mapDispatchToProps, permite determinar que operaciones deberán ser enviadas al componente como props, con la intención de despachar eventos. Si observar, connect pasa tanto el estado del store como las funciones para despachar acciones como props.

Podríamos decir que mapStateToProps es muy parecidos a usar useSelector, con la única diferencia de que mapStateToProps vacia los resultados como props.

Por otra parte, mapDispatchToProps es muy parecido a usar useDispatch, con la única diferencia de que mapDispatchToProps vaciará las operaciones como props.

Por lo demás, Redux funciona exactamente igual tanto en componentes de clase como con el uso de hooks.

Acerca de este libro

Aplicaciones reactivas con React, NodeJS & MongoDB

Todo lo que acabas de ver en este artículo es solo una pequeña parte del libro Aplicaciones reactivas con React, NodeJS & MongoDB, El libro más completo en español para aprender a crear aplicaciones web completas con las tecnologías más potentes de la actualidad, desde el Frontend con React, hasta el Backend con un poderoso API REST con NodeJS y Express y persistiendo todo en MongoDB. te invito a que veas mi libro:

Ver libro
Todos los derechos reservados ©
Reactive programming
LinkedinYoutubeTwitterFacebook

© 2021, Copyright - Oscar Blancarte. All rights reserved.