React Redux – Tutorial para principiantes (IV)

      No hay comentarios en React Redux – Tutorial para principiantes (IV)

Continuamos con la serie de publicaciones donde lo dejamos: http://developinginspanish.com/2020/09/16/react-redux-tutorial-para-principiantes-iii/

Tutorial de React Redux: ¿que es un middleware de Redux?

Hasta ahora vimos los bloques de construcción de Redux: tienda, el policía de tráfico y reductores, que crean el estado en Redux.

Luego están las acciones, los objetos simples de JavaScript, que actúan como mensajeros en su aplicación. Finalmente, tenemos creadores de acciones, funciones para crear esos mensajes.

Affiliated Ad

Ahora, imagine el siguiente escenario: debe evitar que los usuarios creen artículos con palabras específicas dentro del título. Echemos un vistazo a handleSubmit en Form.js:

  handleSubmit(event) {
    event.preventDefault();
    const { title } = this.state;
    this.props.addArticle({ title });
    this.setState({ title: "" });
  }

Podemos simplemente agregar una comprobación antes de this.props.addArticle, ¿verdad? Podría ser algo como esto:

  handleSubmit(event) {
    event.preventDefault();
    const { title } = this.state;
    const forbiddenWords = ['spam', 'money'];
    const foundWord = forbiddenWords.filter(word => title.includes(word) )
    if (foundWord) {
      return this.props.titleForbidden();
    }
    this.props.addArticle({ title });
    this.setState({ title: "" });
  }

¿No era el objetivo de Redux sacar la lógica de nuestros componentes React? ¿Y qué? ¿Podemos verificar la propiedad del título dentro del reductor? ¡Tal vez! Mientras esté allí, enviemos otra acción en respuesta a una palabra prohibida … pero, ¿cómo se supone que debo acceder al envío dentro de un reductor?

Está claro que queremos alcanzar algo diferente. Parece que queremos comprobar la carga útil de la acción (y la propiedad del título) antes de que la acción vaya al reductor. Debe haber una forma de aprovechar el flujo de la aplicación. Adivina qué, eso es exactamente lo que hace un middleware de Redux.

Un middleware de Redux es una función que puede interceptar y actuar en consecuencia nuestras acciones antes de que lleguen al reductor. Si bien la teoría es bastante simple, un middleware de Redux puede parecer un poco confuso.

En su forma básica, un middleware de Redux es una función que devuelve una función, que toma next como parámetro. Luego, la función interna devuelve otra función que toma acción como parámetro y finalmente devuelve siguiente (acción). Así es como se ve:

function forbiddenWordsMiddleware() {
  return function(next){
    return function(action){
      // do your stuff
      return next(action);
    }
  }
}

Este último punto es realmente importante: siempre debe llamar a next (acción) en su middleware. Si lo olvida, Redux se detiene y ninguna otra acción llegará al reductor. next (acción) mueve la aplicación hacia adelante llamando al siguiente middleware en la cadena.

En un middleware también puede acceder a getState y enviar:

function forbiddenWordsMiddleware({ getState, dispatch }) {
  return function(next){
    return function(action){
      // do your stuff
      return next(action);
    }
  }
}

Lo que también es interesante es que el middleware no se cierra después de la siguiente (acción). Si está interesado en leer el siguiente estado de la aplicación después de que se ejecute la cadena de middleware, puede capturarlo con getState después de la siguiente (acción):

function forbiddenWordsMiddleware({ getState, dispatch }) {
  return function(next){
    return function(action){
      // do your stuff
      const nextAction = next(action);
      // read the next state
      const state = getState();
      // return the next action
      return nextAction;  
    }
  }
}

Aún así, no olvide devolver la siguiente acción al final.

Lo sé, quieres llorar y cambiar de carrera, pero ten paciencia conmigo. El middleware en Redux es muy importante porque mantendrá la mayor parte de la lógica de su aplicación. Si lo piensas, no hay mejor lugar que un middleware para abstraer la lógica empresarial.

Armados con ese conocimiento, podemos crear nuestro primer middleware Redux: debería verificar si la carga útil de la acción tiene malas palabras. Veremos la implementación real en la siguiente sección.

Tutorial de React Redux: su primer middleware de Redux

El middleware que vamos a construir debería inspeccionar la carga útil de la acción. Hay muchos beneficios de usar un middleware de Redux:

  • La mayor parte de la lógica puede vivir fuera de la biblioteca de la interfaz de usuario
  • El middleware se convierte en piezas de lógica reutilizables, fáciles de comprender
  • El middleware se puede probar de forma aislada

Entonces, vamos a ensuciarnos las manos. Crea una nueva carpeta para el middleware:

mkdir -p src/js/middleware

Ahora crea un nuevo archivo llamado src/js/middleware/index.js. La estructura de nuestro primer middleware debería ser como:

function forbiddenWordsMiddleware({ dispatch }) {
  return function(next){
    return function(action){
      // do your stuff
      return next(action);
    }
  }
}

Por ahora no necesitamos getState, solo tenemos dispatch como primer parámetro. Bien. Ahora debemos verificar la carga útil de la acción, es decir, la propiedad del título. Si el título coincide con una o más malas palabras, impedimos que el usuario agregue el artículo.

Además, la comprobación debe activarse solo cuando la acción sea de tipo ADD_ARTICLE. Que tiene sentido. Que tal así:

import { ADD_ARTICLE } from "../constants/action-types";

const forbiddenWords = ["spam", "money"];

export function forbiddenWordsMiddleware({ dispatch }) {
  return function(next) {
    return function(action) {
      // do your stuff
      if (action.type === ADD_ARTICLE) {
        
        const foundWord = forbiddenWords.filter(word =>
          action.payload.title.includes(word)
        );

        if (foundWord.length) {
          return dispatch({ type: "FOUND_BAD_WORD" });
        }
      }
      return next(action);
    };
  };
}

Esto es lo que hace el middleware: cuando el tipo de acción es ADD_ARTICLE, verifica si action.payload.title contiene una mala palabra. Si lo hace, envíe una acción de tipo FOUND_BAD_WORD; de lo contrario, deje que se ejecute el siguiente middleware.

Ahora es el momento de conectar el forbiddenWordsMiddleware a la tienda Redux. Para eso necesitamos importar nuestro middleware, otra utilidad de Redux (applyMiddleware), y luego cocinar todo junto.

Abra src/js/store/index.js y modifique el archivo así:

// src/js/store/index.js

import { createStore, applyMiddleware } from "redux";
import rootReducer from "../reducers/index";
import { forbiddenWordsMiddleware } from "../middleware";

const store = createStore(
  rootReducer,
  applyMiddleware(forbiddenWordsMiddleware)
);

export default store;

Nota: si desea habilitar las herramientas de desarrollo de Redux, use este código.

Guarde y cierre el archivo, ejecute npm start y compruebe si el middleware funciona. Intente agregar un artículo con «money» en su título y no verá el nuevo artículo aparecer en la lista.

¡El middleware funciona! ¡Buen trabajo! En las siguientes secciones exploraremos acciones asincrónicas en Redux con Redux Thunk y Redux Saga.

Ver la lección 5 del curso Redux, trabajando con middlewares

Ver la rama en Github

Tutorial de React Redux: acciones asincrónicas en Redux, la forma sencilla

Hasta ahora estábamos tratando con datos sincrónicos. Es decir, el envío de una acción es sincrónico. Sin AJAX, sin promesas. Devolvemos un objeto simple de nuestros creadores de acciones. Y cuando la acción llega al reductor volvemos al siguiente estado.

Ahora, suponga que queremos obtener datos de una API. En React, pondría una llamada en componentDidMount y lo llamaría un día. ¿Pero qué hay de Redux? ¿Cuál es un buen lugar para llamar a funciones asincrónicas? Pensemos un momento en ello.

Reductores De ninguna manera. Los reductores deben mantenerse simples y limpios. Un reductor no es un buen lugar para la lógica asincrónica.

¿Comportamiento? ¿Cómo se supone que debo hacer eso? Las acciones en Redux son objetos simples. ¿Y qué hay de los creadores de acción? Un creador de acciones es una función, y parece un buen lugar para llamar a una API. Vamos a intentarlo.

Crearemos una nueva acción llamada getData. Esta acción llama a una API con Fetch y devuelve una acción Redux.

Abra src/js/actions/index.js y cree una nueva acción llamada getData:

// src/js/actions/index.js

// ...
// our new action creator. Will it work?
export function getData() {
  return fetch("https://jsonplaceholder.typicode.com/posts")
    .then(response => response.json())
    .then(json => {
      return { type: "DATA_LOADED", payload: json };
    });
}

Tiene sentido. ¿Funcionará? Conectemos un componente React para enviar getData desde componentDidMount.mapDispatchToProps (esta vez con la forma abreviada de objetos) mapeará los creadores de acciones de Redux a los accesorios de nuestro componente. Cree un nuevo componente de React en src/js/components/Posts.js:

import { connect } from "react-redux";
import { getData } from "../actions/index";

export class Post extends Component {
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    // calling the new action creator
    this.props.getData();
  }

  render() {
    return null;
  }
}

export default connect(
  null,
  { getData }
)(Post);

Por último, actualice src/js/components/App.js para usar el nuevo componente:

import React from "react";
import List from "./List";
import Form from "./Form";
import Post from "./Posts";

const App = () => (
  <>
    <div>
      <h2>Articles</h2>
      <List />
    </div>
    <div>
      <h2>Add a new article</h2>
      <Form />
    </div>
    <div>
      <h2>API posts</h2>
      <Post />
    </div>
  </>
);

export default App;

Guarde y cierre los archivos, ejecuta la aplicación y observa la consola: «Error: las acciones deben ser objetos simples. Use middleware personalizado para acciones asíncronas». No podemos llamar a Fetch desde un creador de acciones en Redux. ¿Ahora que?

Para que las cosas funcionen, necesitamos un middleware personalizado. Afortunadamente, hay algo listo para nosotros: redux-thunk.

Acciones asíncronas en Redux con Redux Thunk

Acabamos de descubrir que llamar a Fetch desde un creador de acciones no funciona.

Eso es porque Redux espera objetos como acciones, pero estamos tratando de devolver una Promesa.

Con redux-thunk (es un middleware) podemos superar el problema y devolver funciones de los creadores de acciones. De esta manera podemos llamar a las API, retrasar el envío de una acción y más.

Primero necesitamos instalar el middleware con:

npm i redux-thunk --save-dev

Ahora carguemos el middleware en src/js/store/index.js:

import { createStore, applyMiddleware, compose } from "redux";
import rootReducer from "../reducers/index";
import { forbiddenWordsMiddleware } from "../middleware";
import thunk from "redux-thunk";

const storeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(
  rootReducer,
  storeEnhancers(applyMiddleware(forbiddenWordsMiddleware, thunk))
);

export default store;

En este punto, necesitamos refactorizar getData para usar redux-thunk. Abra src/js/actions/index.js y actualice el creador de acciones así:

export function getData() {
  return function(dispatch) {
    return fetch("https://jsonplaceholder.typicode.com/posts")
      .then(response => response.json())
      .then(json => {
        dispatch({ type: "DATA_LOADED", payload: json });
      });
  };
}

¡Eso es redux-thunk! Para acceder al estado dentro del creador de acciones, también puedes agregar getState en la lista del parámetro.

Además, observa el uso del envío en el interior para enviar la siguiente acción después de que se complete la recuperación.

Con eso en su lugar, estamos listos para actualizar nuestro reductor con el nuevo tipo de acción.

Abre src/js/reducers/index.js y agrega una nueva declaración if. También podemos agregar una nueva clave dentro del estado inicial para guardar los artículos de la API:

import { ADD_ARTICLE } from "../constants/action-types";

const initialState = {
  articles: [],
  remoteArticles: []
};

function rootReducer(state = initialState, action) {
  if (action.type === ADD_ARTICLE) {
    return Object.assign({}, state, {
      articles: state.articles.concat(action.payload)
    });
  }

  if (action.type === "DATA_LOADED") {
    return Object.assign({}, state, {
      remoteArticles: state.remoteArticles.concat(action.payload)
    });
  }
  return state;
}

export default rootReducer;

(Lo sé, DATA_LOADED debería ser su propia constante con nombre. Lo dejé como un ejercicio para ti. ¡Espero que no te importe!)

Finalmente, estamos listos para actualizar nuestro componente Post para mostrar nuestras publicaciones «remotas». Usaremos mapStateToProps (nuevamente, siéntete libre de llamar a esta función seleccionar) para seleccionar diez publicaciones:

import React, { Component } from "react";
import { connect } from "react-redux";
import { getData } from "../actions/index";

export class Post extends Component {
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    this.props.getData();
  }

  render() {
    return (
      <ul>
        {this.props.articles.map(el => (
          <li key={el.id}>{el.title}</li>
        ))}
      </ul>
    );
  }
}

function mapStateToProps(state) {
  return {
    articles: state.remoteArticles.slice(0, 10)
  };
}

export default connect(
  mapStateToProps,
  { getData }
)(Post);

Guarda y cierra los archivos, ejecuta la aplicación, ¡y todo debería funcionar bien! ¡Buen trabajo!

En resumen: Redux no comprende otros tipos de acciones que no sean un objeto simple.

Si deseas mover la lógica asincróna de React a Redux y poder devolver funciones en lugar de objetos simples, debes usar un middleware personalizado.

redux-thunk es un middleware para Redux. Con redux-thunk puede devolver funciones de creadores de acciones, no solo objetos. Puede realizar un trabajo asíncrono dentro de sus acciones y enviar otras acciones en respuesta a las llamadas AJAX.

¿Cuándo usar redux-thunk? redux-thunk es un buen middleware que funciona muy bien para la mayoría de los casos de uso. Pero, si su lógica asíncróna implica escenarios más complejos, o si tiene requisitos específicos, entonces redux saga podría ser una mejor opción.

En la siguiente sección lo veremos. ¡Agárrate fuerte!

Vea la lección 6 del curso Redux, acciones asincrónicas con redux-thunk

Ver la rama en Github

Tutorial de React Redux: introducción a Redux Saga

redux-thunk tiene mucho sentido para muchos proyectos. También puedes omitir por completo redux-thunk y mover tu lógica asíncrona a un middleware personalizado. Pero, en realidad, las acciones asíncronas pueden ser más difíciles de probar y organizar.

Por esta razón, la mayoría de los desarrolladores prefieren un enfoque alternativo: redux-saga.

¿Qué es redux-saga? redux-saga es un middleware de Redux para controlar los efectos secundarios. Con redux-saga puede tener un hilo separado en su aplicación para lidiar con acciones impuras: llamadas a API, acceso al almacenamiento y más.

redux-saga es diferente de una acción asíncronca en términos de sintaxis y organización del código. Con redux-thunk puede poner una llamada a la API directamente dentro de un creador de acciones, mientras que en redux-saga puede tener una clara separación entre la lógica síncrona y asíncrona.

Cabe señalar que redux-saga no utiliza la función JavaScript normal. Verás muchos asteriscos y «yield» en tus sagas. ¡Esos asteriscos marcan las funciones del generador!

Las funciones del generador en JavaScript son funciones que se pueden pausar y reanudar bajo demanda. redux-saga se basa en gran medida en las funciones del generador, pero lo bueno es que no necesitarás llamar a next() en tu código. redux-saga se encarga de eso por debajo.

Escribiendo tu primer Redux Saga

En esta sección refactorizaremos nuestro código para usar una saga de Redux en lugar de un procesador. No cubriré toda la API de Saga en esta publicación, así que tened paciencia conmigo. Solo echaremos un vistazo a varios métodos.

Antes de comenzar, instala redux saga con:

npm i redux-saga --save-dev

Ahora podemos refactorizar nuestra acción asíncrona y eliminar la llamada de recuperación. A partir de ahora, nuestro creador de acciones sólo enviará una acción simple. Abra src/js/actions/index.js y modifique getData para devolver una acción simple llamada DATA_REQUESTED:

export function getData() {
  return { type: "DATA_REQUESTED" };
}

La acción DATA_REQUESTED será interceptada por Redux Saga con takeEvery. Puede imaginar takeEvery tomando cada acción DATA_REQUESTED pasando dentro de nuestra aplicación y comenzando a trabajar en respuesta a esa acción.

Ahora bien, ¿cómo se estructura una saga? Una saga redux podría vivir en un solo archivo que contenga:

  • Una función worker
  • Una función watcher

El observador (watcher) es una función generadora que observa cada acción que nos interesa. En respuesta a esa acción, el observador llamará a una saga de trabajadores, que es otra función generadora para realizar la llamada API real.

El trabajador (worker) ( llamará a la API remota con una llamada de redux-saga/effects. Cuando se cargan los datos, podemos enviar otra acción de nuestra saga con put, nuevamente, de redux-saga/effects.

Armados con este conocimiento podemos establecer nuestro primero Redux Saga. Primero crea una nueva carpeta para guardar tus sagas:

mkdir -p src/js/sagas

Luego crea un nuevo archivo llamado api-saga.js en src/js/sagas. Y aquí está nuestra saga:

import { takeEvery, call, put } from "redux-saga/effects";

export default function* watcherSaga() {
  yield takeEvery("DATA_REQUESTED", workerSaga);
}

function* workerSaga() {
  try {
    const payload = yield call(getData);
    yield put({ type: "DATA_LOADED", payload });
  } catch (e) {
    yield put({ type: "API_ERRORED", payload: e });
  }
}

Tomemos un descanso para leer el flujo lógico de nuestra saga.

Desmitificando tu primer Redux Saga

Echa un vistazo al código de arriba. Así es como funciona:

  1. Toma cada acción llamada DATA_REQUESTED y para cada acción arranca un worker de saga
  2. Dentro del worker de saga, llama a una función llamada getData
  3. Si la función tiene éxito, entonces envía (put) una nueva acción llamada DATA_LOADED junto con una carga útil
  4. Si la función falla, entonces envía (put) una nueva acción llamada API_ERRORED, junto con una carga útil (el error)

Lo único que nos falta en nuestro código es getData. Abre src/js/sagas/ api-saga.js nuevamente y agrega la función:

import { takeEvery, call, put } from "redux-saga/effects";

export default function* watcherSaga() {
  yield takeEvery("DATA_REQUESTED", workerSaga);
}

function* workerSaga() {
  try {
    const payload = yield call(getData);
    yield put({ type: "DATA_LOADED", payload });
  } catch (e) {
    yield put({ type: "API_ERRORED", payload: e });
  }
}

function getData() {
  return fetch("https://jsonplaceholder.typicode.com/posts").then(response =>
    response.json()
  );
}

Finalmente podemos conectar Redux Saga a nuestra tienda redux. Abre src/js/store/index.js y actualiza la tienda de la siguiente manera:

import { createStore, applyMiddleware, compose } from "redux";
import rootReducer from "../reducers/index";
import { forbiddenWordsMiddleware } from "../middleware";
import createSagaMiddleware from "redux-saga";
import apiSaga from "../sagas/api-saga";

const initialiseSagaMiddleware = createSagaMiddleware();

const storeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(
  rootReducer,
  storeEnhancers(
    applyMiddleware(forbiddenWordsMiddleware, initialiseSagaMiddleware)
  )
);

initialiseSagaMiddleware.run(apiSaga);

export default store;

Observa createSagaMiddleware e initialiseSagaMiddleware.run para ejecutar nuestra saga. Ahora cierra y guarda el archivo. Ejecuta npm start y deberías ver exactamente la misma salida nuevamente con las publicaciones remotas que se muestran correctamente en el navegador.

¡Felicidades! ¡Creaste tu primera saga redux! Y ahora un par de ejercicios para ti:

  • Nuestro reductor estaba listo para manejar DATA_LOADED junto con su carga útil. Completa el reductor para lidiar con API_ERRORED.
  • Mueve DATA_LOADED, API_ERRORED y DATA_REQUESTED a constantes con nombre.
  • ¿Necesitamos tener mejor en cuenta los errores de recuperación dentro de getData?

CÓDIGO: puedes acceder al ejemplo completo en react-redux-tutorial en Github. Clone el repositorio y verifique la rama más reciente:

git clone https://github.com/valentinogagliardi/react-redux-tutorial
cd react-redux-tutorial
git checkout your-first-redux-saga

Ver la rama en Github

Sagas y parámetros

Los workers de saga toman action como parámetro:

function* workerSaga(action) {
// omit
}

Eso significa que podemos usar una carga útil de acción si está presente. Entonces, si tu componente Post envía una acción con su carga útil (mira componentDidMount):

// src/js/components/Posts.js

import React, { Component } from "react";
import { connect } from "react-redux";
import { getData } from "../actions/index";

export class Post extends Component {
  componentDidMount() {
    this.props.getData("https://api.valentinog.com/api/link/");
  }

  render() {
    return (
      <ul>
        {this.props.articles.map(el => (
          <li key={el.id}>{el.title}</li>
        ))}
      </ul>
    );
  }
}

function mapStateToProps(state) {
  return {
    articles: state.remoteArticles.slice(0, 10)
  };
}

export default connect(mapStateToProps, { getData })(Post);

y el creador de la acción también devuelve la carga útil:

// src/js/actions/index.js

export function getData(url) {
  return { type: "DATA_REQUESTED", payload: { url } };
}

Podemos acceder a la acción en nuestra saga de trabajadores y pasar la carga útil de la acción (url en este caso) a getData:

import { takeEvery, call, put } from "redux-saga/effects";

export default function* watcherSaga() {
  yield takeEvery("DATA_REQUESTED", workerSaga);
}

function* workerSaga(action) {
  try {
    // pass the action payload to getData
    const payload = yield call(getData, action.payload.url);
    yield put({ type: "DATA_LOADED", payload });
  } catch (e) {
    yield put({ type: "API_ERRORED", payload: e });
  }
}

function getData(url) {
  return fetch(url).then(response => response.json());
}

Hasta aquí la cuarta parte de este tutorial de React Redux. Permanece atento, pronto publicaremos la quinta parte.

Puedes leer el artículo original en inglés en: https://www.valentinog.com/blog/redux/#react-redux-tutorial-who-this-guide-is-for

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *