Una guía ficticia para Redux y Thunk en React

Si, como yo, has leído los documentos de Redux, miraste los videos de Dan, hiciste el curso de Wes y aún no entendiste bien cómo usar Redux, espero que esto ayude.

Me tomó algunos intentos usar Redux antes de hacer clic, así que pensé en escribir el proceso de conversión de una aplicación existente que busca JSON para usar Redux y Redux Thunk. Si no sabe qué es Thunk, no se preocupe demasiado, pero lo usaremos para hacer llamadas asincrónicas en la "forma Redux".

Este tutorial asume que tiene una comprensión básica de React y ES6 / 2015, pero esperamos que sea lo suficientemente fácil de seguir independientemente.

La forma no Redux

Comencemos por crear un componente React en components / ItemList.js para buscar y mostrar una lista de elementos.

Sentando las bases

Primero configuraremos un componente estático con un estado que contiene varios elementos para generar, y 2 estados booleanos para representar algo diferente cuando se carga o con errores, respectivamente.

import React, {Component} de 'react';
class ItemList extiende Componente {
    constructor () {
        súper();
        this.state = {
            artículos: [
                {
                    id: 1
                    etiqueta: 'Listar elemento 1'
                },
                {
                    id: 2
                    etiqueta: 'Listar elemento 2'
                },
                {
                    id: 3
                    etiqueta: 'Listar elemento 3'
                },
                {
                    id: 4
                    etiqueta: 'Listar elemento 4'
                }
            ],
            hasErrored: falso,
            isLoading: falso
        };
    }
    render () {
        if (this.state.hasErrored) {
            volver 

¡Lo siento! Se produjo un error al cargar los elementos

;         }
        if (this.state.isLoading) {
            return 

Cargando ...

;         }
        regreso (
            
                    {this.state.items.map ((item) => (                     
  •                         {item.label}                     
  •                 ))}             
        );     } }
exportar ItemList predeterminado;

Puede que no parezca mucho, pero este es un buen comienzo.

Cuando se procesa, el componente debe generar 4 elementos de la lista, pero si tuviera que establecer isLoading o hasErrored en true, se generaría un

relevante.

Haciéndolo dinámico

La codificación dura de los elementos no constituye un componente muy útil, por lo tanto, obtengamos los elementos de una API JSON, que también nos permitirá establecer isLoading y hasErrored según corresponda.

La respuesta será idéntica a nuestra lista codificada de artículos, pero en el mundo real, podría obtener una lista de los libros más vendidos, las últimas publicaciones de blog o lo que sea adecuado para su aplicación.

Para buscar los elementos, vamos a utilizar la API de recuperación de Fetch. Fetch hace que las solicitudes sean mucho más fáciles que el clásico XMLHttpRequest y devuelve una promesa de la respuesta resuelta (que es importante para Thunk). Fetch no está disponible en todos los navegadores, por lo que deberá agregarlo como una dependencia a su proyecto con:

npm install whatwg-fetch --save

La conversión es realmente bastante simple.

  • Primero estableceremos nuestros elementos iniciales en una matriz vacía []
  • Ahora agregaremos un método para obtener los datos y establecer los estados de carga y error:
fetchData (url) {
    this.setState ({isLoading: true});
    buscar (url)
        .then ((respuesta) => {
            if (! response.ok) {
                lanzar error (response.statusText);
            }
            this.setState ({isLoading: false});
            respuesta de retorno;
        })
        .then ((respuesta) => respuesta.json ())
        .then ((items) => this.setState ({items})) // Valor abreviado de la propiedad ES6 para {items: items}
        .catch (() => this.setState ({hasErrored: true}));
}
  • Luego lo llamaremos cuando se monte el componente:
componentDidMount () {
  this.fetchData ('http://5826ed963900d612000138bd.mockapi.io/items');
}

Lo que nos deja con (líneas sin cambios omitidas):

class ItemList extiende Componente {
    constructor () {
        this.state = {
            artículos: [],
        };
    }
    fetchData (url) {
        this.setState ({isLoading: true});
        buscar (url)
            .then ((respuesta) => {
                if (! response.ok) {
                    lanzar error (response.statusText);
                }
                this.setState ({isLoading: false});
                respuesta de retorno;
            })
            .then ((respuesta) => respuesta.json ())
            .then ((items) => this.setState ({items}))
            .catch (() => this.setState ({hasErrored: true}));
    }
    componentDidMount () {
        this.fetchData ('http://5826ed963900d612000138bd.mockapi.io/items');
    }
    render () {
    }
}

Y eso es. ¡Su componente ahora recupera los elementos desde un punto final REST! Es de esperar que aparezca "Cargando ..." aparece brevemente antes de los 4 elementos de la lista. Si pasa una URL rota para obtener datos, debería ver nuestro mensaje de error.

Sin embargo, en realidad, un componente no debe incluir la lógica para obtener datos, y los datos no deben almacenarse en el estado de un componente, por lo que aquí es donde entra Redux.

Convertir a Redux

Para comenzar, necesitamos agregar Redux, React Redux y Redux Thunk como dependencias de nuestro proyecto para que podamos usarlos. Podemos hacer eso con:

npm install redux react-redux redux-thunk --save

Entendiendo Redux

Hay algunos principios básicos para Redux que debemos entender:

  1. Hay 1 objeto de estado global que administra el estado de toda su aplicación. En este ejemplo, se comportará de manera idéntica al estado de nuestro componente inicial. Es la única fuente de verdad.
  2. La única forma de modificar el estado es emitiendo una acción, que es un objeto que describe lo que debería cambiar. Los creadores de acciones son las funciones que se envían para emitir un cambio; todo lo que hacen es devolver una acción.
  3. Cuando se despacha una acción, un reductor es la función que realmente cambia el estado apropiado para esa acción, o devuelve el estado existente si la acción no es aplicable a ese reductor.
  4. Los reductores son "funciones puras". No deberían tener ningún efecto secundario ni mutar el estado; deben devolver una copia modificada.
  5. Los reductores individuales se combinan en un solo rootReducer para crear las propiedades discretas del estado.
  6. La Tienda es lo que lo reúne todo: representa el estado mediante el uso de rootReducer, cualquier middleware (Thunk en nuestro caso), y le permite realmente despachar acciones.
  7. Para usar Redux en React, el componente envuelve toda la aplicación y pasa el storedown a todos los niños.

Todo esto debería aclararse a medida que comenzamos a convertir nuestra aplicación para usar Redux.

Diseñando nuestro estado

Por el trabajo que ya hemos realizado, sabemos que nuestro estado necesita tener 3 propiedades: items, hasErrored y isLoading para que esta aplicación funcione como se espera en todas las circunstancias, lo que se correlaciona con la necesidad de 3 acciones únicas.

Ahora, aquí es por qué los Creadores de acciones son diferentes a las Acciones y no necesariamente tienen una relación 1: 1: necesitamos un cuarto creador de acciones que llame a nuestras otras 3 acciones (creadores) dependiendo del estado de la obtención de los datos. Este cuarto creador de acciones es casi idéntico a nuestro método fetchData () original, pero en lugar de establecer directamente el estado con this.setState ({isLoading: true}), enviaremos una acción para hacer lo mismo: dispatch (isLoading (true )).

Creando nuestras acciones

Creemos un archivo actions / items.js para contener a nuestros creadores de acciones. Comenzaremos con nuestras 3 acciones simples.

exportar elementos de función HasErrored (bool) {
    regreso {
        tipo: 'ITEMS_HAS_ERRORED',
        hasErrored: bool
    };
}
exportar elementos de función IsLoading (bool) {
    regreso {
        tipo: 'ITEMS_IS_LOADING',
        isLoading: bool
    };
}
Función de exportación itemsFetchDataSuccess (items) {
    regreso {
        tipo: 'ITEMS_FETCH_DATA_SUCCESS',
        artículos
    };
}

Como se mencionó anteriormente, los creadores de acciones son funciones que devuelven una acción. Exportamos cada uno para poder usarlos en otra parte de nuestra base de código.

Los primeros 2 creadores de acciones toman un bool (verdadero / falso) como argumento y devuelven un objeto con un tipo significativo y el bool asignado a la propiedad apropiada.

El tercero, itemsFetchDataSuccess (), se llamará cuando los datos se hayan obtenido con éxito, con los datos pasados ​​como elementos. A través de la magia de las abreviaturas de valor de propiedad de ES6, devolveremos un objeto con una propiedad llamada elementos cuyo valor será la matriz de elementos;

Nota: el valor que utiliza para el tipo y el nombre de la otra propiedad que se devuelve es importante, ya que los reutilizará en sus reductores

Ahora que tenemos las 3 acciones que representarán nuestro estado, convertiremos el método fetchData de nuestro componente original en un creador de acciones itemsFetchData ().

De forma predeterminada, los creadores de acciones de Redux no admiten acciones asincrónicas como la obtención de datos, por lo que aquí es donde utilizamos Redux Thunk. Thunk te permite escribir creadores de acciones que devuelven una función en lugar de una acción. La función interna puede recibir los métodos de envío dispatch y getState como parámetros, pero solo usaremos dispatch.

Un ejemplo realmente simple sería activar manualmente itemsHasErrored () después de 5 segundos.

función de exportación errorAfterFiveSeconds () {
    // Devolvemos una función en lugar de un objeto de acción
    return (despacho) => {
        setTimeout (() => {
            // Esta función puede enviar otros creadores de acciones
            dispatch (itemsHasErrored (true));
        }, 5000);
    };
}

Ahora que sabemos qué es un thunk, podemos escribir itemsFetchData ().

exportar elementos de función fetchData (url) {
    return (despacho) => {
        despacho (itemsIsLoading (verdadero));
        buscar (url)
            .then ((respuesta) => {
                if (! response.ok) {
                    lanzar error (response.statusText);
                }
                despacho (itemsIsLoading (falso));
                respuesta de retorno;
            })
            .then ((respuesta) => respuesta.json ())
            .then ((items) => dispatch (itemsFetchDataSuccess (items)))
            .catch (() => dispatch (itemsHasErrored (true)));
    };
}

Creando nuestros reductores

Con nuestros creadores de acciones definidos, ahora escribimos reductores que toman estas acciones y devuelven un nuevo estado de nuestra aplicación.

Nota: en Redux, se llama a todos los reductores independientemente de la acción, por lo que dentro de cada uno debe devolver el estado original si la acción no es aplicable.

Cada reductor toma 2 parámetros: el estado (estado) anterior y un objeto de acción. También podemos usar una función ES6 llamada parámetros predeterminados para establecer el estado inicial predeterminado.

Dentro de cada reductor, usamos una instrucción switch para determinar cuándo coincide un action.type. Si bien puede parecer innecesario en estos reductores simples, sus reductores teóricamente podrían tener muchas condiciones, por lo que si / si if / else se volverán desordenados rápidamente.

Si el tipo de acción coincide, entonces devolvemos la propiedad de acción relevante. Como se mencionó anteriormente, el tipo y la acción [propertyName] es lo que se definió en sus creadores de acciones.

Bien, sabiendo esto, creemos nuestros reductores de artículos en reductores / items.js.

exportar elementos de función HasErrored (estado = falso, acción) {
    switch (action.type) {
        caso 'ITEMS_HAS_ERRORED':
            acción de retorno tiene error;
        defecto:
            estado de retorno;
    }
}
elementos de la función de exportaciónIsLoading (estado = falso, acción) {
    switch (action.type) {
        caso 'ITEMS_IS_LOADING':
            acción de retorno.
        defecto:
            estado de retorno;
    }
}
exportar elementos de función (estado = [], acción) {
    switch (action.type) {
        caso 'ITEMS_FETCH_DATA_SUCCESS':
            return action.items;
        defecto:
            estado de retorno;
    }
}

Observe cómo cada reductor lleva el nombre de la propiedad de estado de la tienda resultante, con la acción.tipo que no necesariamente tiene que corresponder. Con suerte, los primeros 2 reductores tienen mucho sentido, pero el último, items (), es ligeramente diferente.

Esto se debe a que podría tener múltiples condiciones que siempre devolverían una matriz de elementos: podría devolver todo en el caso de una recuperación exitosa, podría devolver un subconjunto de elementos después de que se envíe una acción de eliminación, o podría devolver una matriz vacía si todo se elimina

Para repetir, cada reductor devolverá una propiedad discreta del estado, independientemente de cuántas condiciones haya dentro de ese reductor. Inicialmente, me llevó un tiempo entenderlo.

Con los reductores individuales creados, necesitamos combinarlos en un rootReducer para crear un solo objeto.

Cree un nuevo archivo en reductores / index.js.

importar {combineReducers} desde 'redux';
import {items, itemsHasErrored, itemsIsLoading} desde './items';
exportar combineReducers predeterminados ({
    artículos,
    itemsHasErrored,
    itemsIsLoading
});

Importamos cada uno de los reductores de items.js y los exportamos con combineReducers () de Redux. Como nuestros nombres de reductor son idénticos a los que queremos usar para los nombres de propiedades de una tienda, podemos usar la abreviatura ES6.

Observe cómo prefijo intencionalmente mis nombres de reductor, de modo que cuando la aplicación crece en complejidad, no estoy limitado por tener una propiedad "global" hasErrored o isLoading. Es posible que tenga muchas características diferentes que podrían producir un error o estar en un estado de carga, por lo que prefijar las importaciones y luego exportarlas le dará al estado de su aplicación mayor granularidad y flexibilidad. Por ejemplo:

importar {combineReducers} desde 'redux';
import {items, itemsHasErrored, itemsIsLoading} desde './items';
import {posts, postsHasErrored, postsIsLoading} desde './posts';
exportar combineReducers predeterminados ({
    artículos,
    itemsHasErrored,
    itemsIsLoading,
    publicaciones,
    postsHasErrored,
    postsIsLoading
});

Alternativamente, puede usar alias de los métodos en la importación, pero prefiero la coherencia en todos los ámbitos.

Configure la tienda y proporciónela a su aplicación

Esto es bastante sencillo. Vamos a crear store / configureStore.js con:

importar {createStore, applyMiddleware} desde 'redux';
importar thunk desde 'redux-thunk';
importar rootReducer desde '../reducers';
función predeterminada de exportación configureStore (initialState) {
    volver createStore (
        rootReducer,
        estado inicial,
        applyMiddleware (thunk)
    );
}

Ahora cambie el index.js de nuestra aplicación para incluir , configureStore, configure nuestra tienda y ajuste nuestra aplicación () para pasar la tienda como accesorios:

importar Reaccionar desde 'reaccionar';
importar {render} desde 'react-dom';
importar {Proveedor} desde 'react-redux';
importar configureStore desde './store/configureStore';
importar ItemList desde './components/ItemList';
const store = configureStore (); // También puedes pasar un Estado inicial aquí
hacer(
    
        
    ,
    document.getElementById ('aplicación')
);

Lo sé, ha llevado un gran esfuerzo llegar a esta etapa, pero con la configuración completa, ahora podemos modificar nuestro componente para hacer uso de lo que hemos hecho.

Convertir nuestro componente para usar la tienda y los métodos de Redux

Volvamos a componentes / ItemList.js.

En la parte superior del archivo, importe lo que necesitamos:

import {connect} de 'react-redux';
importar {itemsFetchData} desde '../actions/items';

conectar es lo que nos permite conectar un componente a la tienda de Redux, y itemsFetchData es el creador de acciones que escribimos anteriormente. Solo necesitamos importar este creador de una acción, ya que se encarga de despachar las otras acciones.

Después de la definición de clase de nuestro componente, vamos a asignar el estado de Redux y el envío de nuestro creador de acción a los accesorios.

Creamos una función que acepta el estado y luego devuelve un objeto de accesorios. En un componente simple como este, elimino el prefijo para los accesorios has / is, ya que es obvio que están relacionados con elementos.

const mapStateToProps = (estado) => {
    regreso {
        artículos: state.items,
        hasErrored: state.itemsHasErrored,
        isLoading: state.itemsIsLoading
    };
};

Y luego necesitamos otra función para poder enviar nuestro creador de acción itemsFetchData () con un accesorio.

const mapDispatchToProps = (despacho) => {
    regreso {
        fetchData: (url) => dispatch (itemsFetchData (url))
    };
};

Nuevamente, eliminé el prefijo de elementos de la propiedad del objeto devuelto. Aquí fetchData es una función que acepta un parámetro de url y devuelve el envío de itemsFetchData (url).

Ahora, estos 2 mapStateToProps () y mapDispatchToProps () todavía no hacen nada, por lo que debemos cambiar nuestra línea de exportación final a:

exportar conexión predeterminada (mapStateToProps, mapDispatchToProps) (ItemList);

Esto conecta nuestra ItemList a Redux mientras asigna los accesorios para que los usemos.

El último paso es convertir nuestro componente para usar accesorios en lugar de estado, y eliminar las sobras.

  • Elimine los métodos constructor () {} y fetchData () {} ya que ahora son innecesarios.
  • Cambie this.fetchData () en componentDidMount () a this.props.fetchData ().
  • Cambie this.state.X a this.props.X para .hasErrored, .isLoading y .items.

Su componente ahora debería verse así:

import React, {Component} de 'react';
import {connect} de 'react-redux';
importar {itemsFetchData} desde '../actions/items';
class ItemList extiende Componente {
    componentDidMount () {
        this.props.fetchData ('http://5826ed963900d612000138bd.mockapi.io/items');
    }
    render () {
        if (this.props.hasErrored) {
            volver 

¡Lo siento! Se produjo un error al cargar los elementos

;         }
        if (this.props.isLoading) {
            return 

Cargando ...

;         }
        regreso (
            
                    {this.props.items.map ((item) => (                     
  •                         {item.label}                     
  •                 ))}             
        );     } }
const mapStateToProps = (estado) => {
    regreso {
        artículos: state.items,
        hasErrored: state.itemsHasErrored,
        isLoading: state.itemsIsLoading
    };
};
const mapDispatchToProps = (despacho) => {
    regreso {
        fetchData: (url) => dispatch (itemsFetchData (url))
    };
};
exportar conexión predeterminada (mapStateToProps, mapDispatchToProps) (ItemList);

¡Y eso es! ¡La aplicación ahora usa Redux y Redux Thunk para buscar y mostrar los datos!

Eso no fue demasiado difícil, ¿verdad?

Y ahora eres un maestro de Redux: D

¿Qué sigue?

Puse todo este código en GitHub, con confirmaciones para cada paso. Quiero que lo clone, lo ejecute y lo entienda, luego agregue la capacidad para que el usuario elimine elementos de la lista individual en función del índice del elemento.

Todavía no he mencionado realmente que en Redux, el estado es inmutable, lo que significa que no puede modificarlo, por lo que debe devolver un nuevo estado en sus reductores. Los 3 reductores que escribimos anteriormente eran simples y "simplemente funcionaron", pero eliminar elementos de una matriz requiere un enfoque con el que puede no estar familiarizado.

Ya no puede usar Array.prototype.splice () para eliminar elementos de una matriz, ya que eso mutará la matriz original. Dan explica cómo eliminar un elemento de una matriz en este video, pero si te quedas atascado, puedes revisar (juego de palabras) la rama de eliminar elementos para la solución.

Realmente espero que esto haya aclarado el concepto de Redux y Thunk y cómo podría convertir una aplicación React existente para usarlos. Sé que escribir esto ha solidificado mi comprensión, así que estoy muy feliz de haberlo hecho.

Todavía te recomiendo leer los documentos de Redux, ver los videos de Dan y volver a hacer el curso de Wes, ya que espero que ahora puedas entender algunos de los otros principios más complejos y profundos.

Este artículo ha sido publicado en Codepen para un mejor formato de código.