¿Por qué necesitamos middleware para el flujo asíncrono en Redux?

685

Según los documentos, "Sin middleware, la tienda Redux solo admite el flujo de datos síncrono" . No entiendo por qué este es el caso. ¿Por qué el componente contenedor no puede llamar a la API asíncrona y luego a dispatchlas acciones?

Por ejemplo, imagine una interfaz de usuario simple: un campo y un botón. Cuando el usuario presiona el botón, el campo se llena con datos de un servidor remoto.

Un campo y un botón

import * as React from 'react';
import * as Redux from 'redux';
import { Provider, connect } from 'react-redux';

const ActionTypes = {
    STARTED_UPDATING: 'STARTED_UPDATING',
    UPDATED: 'UPDATED'
};

class AsyncApi {
    static getFieldValue() {
        const promise = new Promise((resolve) => {
            setTimeout(() => {
                resolve(Math.floor(Math.random() * 100));
            }, 1000);
        });
        return promise;
    }
}

class App extends React.Component {
    render() {
        return (
            <div>
                <input value={this.props.field}/>
                <button disabled={this.props.isWaiting} onClick={this.props.update}>Fetch</button>
                {this.props.isWaiting && <div>Waiting...</div>}
            </div>
        );
    }
}
App.propTypes = {
    dispatch: React.PropTypes.func,
    field: React.PropTypes.any,
    isWaiting: React.PropTypes.bool
};

const reducer = (state = { field: 'No data', isWaiting: false }, action) => {
    switch (action.type) {
        case ActionTypes.STARTED_UPDATING:
            return { ...state, isWaiting: true };
        case ActionTypes.UPDATED:
            return { ...state, isWaiting: false, field: action.payload };
        default:
            return state;
    }
};
const store = Redux.createStore(reducer);
const ConnectedApp = connect(
    (state) => {
        return { ...state };
    },
    (dispatch) => {
        return {
            update: () => {
                dispatch({
                    type: ActionTypes.STARTED_UPDATING
                });
                AsyncApi.getFieldValue()
                    .then(result => dispatch({
                        type: ActionTypes.UPDATED,
                        payload: result
                    }));
            }
        };
    })(App);
export default class extends React.Component {
    render() {
        return <Provider store={store}><ConnectedApp/></Provider>;
    }
}

Cuando se procesa el componente exportado, puedo hacer clic en el botón y la entrada se actualiza correctamente.

Tenga updateen cuenta la función en la connectllamada. Envía una acción que le dice a la aplicación que se está actualizando y luego realiza una llamada asíncrona. Una vez que finaliza la llamada, el valor proporcionado se distribuye como una carga útil de otra acción.

¿Qué hay de malo en este enfoque? ¿Por qué querría usar Redux Thunk o Redux Promise, como sugiere la documentación?

EDITAR: Busqué pistas en el repositorio de Redux y descubrí que los creadores de acción debían ser funciones puras en el pasado. Por ejemplo, aquí hay un usuario que intenta proporcionar una mejor explicación para el flujo de datos asíncrono:

El creador de la acción en sí sigue siendo una función pura, pero la función thunk que devuelve no necesita serlo, y puede hacer nuestras llamadas asíncronas

Los creadores de acciones ya no están obligados a ser puros. Entonces, el middleware thunk / promise definitivamente se requería en el pasado, pero parece que este ya no es el caso.

sbichenko
fuente
53
Los creadores de acciones nunca tuvieron que ser funciones puras. Fue un error en los documentos, no una decisión que cambió.
Dan Abramov
1
@DanAbramov para la comprobabilidad, sin embargo, puede ser una buena práctica. Redux-saga lo permite: stackoverflow.com/a/34623840/82609
Sebastien Lorber el

Respuestas:

700

¿Qué hay de malo en este enfoque? ¿Por qué querría usar Redux Thunk o Redux Promise, como sugiere la documentación?

No hay nada malo con este enfoque. Es simplemente inconveniente en una aplicación grande porque tendrás diferentes componentes que realizan las mismas acciones, es posible que desees eliminar algunas acciones o mantener algún estado local como ID de incremento automático cerca de los creadores de acciones, etc. Por lo tanto, es más fácil El punto de vista de mantenimiento para extraer creadores de acciones en funciones separadas.

Puede leer mi respuesta a "Cómo enviar una acción de Redux con un tiempo de espera" para obtener un tutorial más detallado.

El middleware como Redux Thunk o Redux Promise solo le da "azúcar de sintaxis" para enviar thunks o promesas, pero no tiene que usarlo.

Entonces, sin ningún middleware, su creador de acción podría verse así

// action creator
function loadData(dispatch, userId) { // needs to dispatch, so it is first argument
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch
}

Pero con Thunk Middleware puedes escribirlo así:

// action creator
function loadData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`) // Redux Thunk handles these
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  this.props.dispatch(loadData(this.props.userId)); // dispatch like you usually do
}

Entonces no hay una gran diferencia. Una cosa que me gusta del último enfoque es que al componente no le importa que el creador de la acción sea asíncrono. Simplemente llama dispatchnormalmente, también se puede usar mapDispatchToPropspara vincular a tal creador de acción con una sintaxis corta, etc. Los componentes no saben cómo se implementan los creadores de acción, y puede cambiar entre diferentes enfoques asincrónicos (Redux Thunk, Redux Promise, Redux Saga ) sin cambiar los componentes. Por otro lado, con el primer enfoque explícito, sus componentes saben exactamente que una llamada específica es asíncrona y debe dispatchser aprobada por alguna convención (por ejemplo, como un parámetro de sincronización).

También piense en cómo cambiará este código. Supongamos que queremos tener una segunda función de carga de datos y combinarlos en un solo creador de acciones.

Con el primer enfoque, debemos tener en cuenta a qué tipo de creador de acción estamos llamando:

// action creators
function loadSomeData(dispatch, userId) {
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
    );
}
function loadOtherData(dispatch, userId) {
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
    );
}
function loadAllData(dispatch, userId) {
  return Promise.all(
    loadSomeData(dispatch, userId), // pass dispatch first: it's async
    loadOtherData(dispatch, userId) // pass dispatch first: it's async
  );
}


// component
componentWillMount() {
  loadAllData(this.props.dispatch, this.props.userId); // pass dispatch first
}

Con Redux Thunk, los creadores de acciones pueden ser dispatchel resultado de otros creadores de acciones y ni siquiera pensar si son síncronos o asíncronos:

// action creators
function loadSomeData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
    );
}
function loadOtherData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
    );
}
function loadAllData(userId) {
  return dispatch => Promise.all(
    dispatch(loadSomeData(userId)), // just dispatch normally!
    dispatch(loadOtherData(userId)) // just dispatch normally!
  );
}


// component
componentWillMount() {
  this.props.dispatch(loadAllData(this.props.userId)); // just dispatch normally!
}

Con este enfoque, si luego desea que sus creadores de acciones analicen el estado actual de Redux, puede usar el segundo getStateargumento pasado a los thunks sin modificar el código de llamada en absoluto:

function loadSomeData(userId) {
  // Thanks to Redux Thunk I can use getState() here without changing callers
  return (dispatch, getState) => {
    if (getState().data[userId].isLoaded) {
      return Promise.resolve();
    }

    fetch(`http://data.com/${userId}`)
      .then(res => res.json())
      .then(
        data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
        err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
      );
  }
}

Si necesita cambiarlo para que sea sincrónico, también puede hacerlo sin cambiar ningún código de llamada:

// I can change it to be a regular action creator without touching callers
function loadSomeData(userId) {
  return {
    type: 'LOAD_SOME_DATA_SUCCESS',
    data: localStorage.getItem('my-data')
  }
}

Entonces, el beneficio de usar middleware como Redux Thunk o Redux Promise es que los componentes no son conscientes de cómo se implementan los creadores de acciones, y si les importa el estado de Redux, si son síncronos o asíncronos, y si llaman o no a otros creadores de acciones . La desventaja es un poco indirecta, pero creemos que vale la pena en aplicaciones reales.

Finalmente, Redux Thunk y sus amigos son solo un posible enfoque para las solicitudes asincrónicas en las aplicaciones de Redux. Otro enfoque interesante es Redux Saga, que le permite definir demonios de larga duración ("sagas") que toman acciones a medida que se presentan y transforman o realizan solicitudes antes de generar acciones. Esto mueve la lógica de los creadores de acción a las sagas. Es posible que desee verlo y luego elegir lo que más le convenga.

Busqué pistas en el repositorio de Redux y descubrí que los creadores de acción debían ser funciones puras en el pasado.

Esto es incorrecto. Los documentos dijeron esto, pero los documentos estaban equivocados.
Los creadores de acciones nunca tuvieron que ser funciones puras.
Arreglamos los documentos para reflejar eso.

Dan Abramov
fuente
57
Tal vez una forma corta de decir que el pensamiento de Dan es: el middleware es un enfoque centralizado, de esta manera le permite mantener sus componentes más simples y generalizados y controlar el flujo de datos en un solo lugar. Si mantiene una gran aplicación, la disfrutará =)
Sergey Lapin
3
@asdfasdfads No veo por qué no funcionaría. Funcionaría exactamente igual; poner alertdespués dispatch()de la acción.
Dan Abramov
99
Penúltima línea en su primer ejemplo de código: loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch. ¿Por qué necesito pasar en despacho? Si por convención solo hay una única tienda global, ¿por qué no hago referencia a eso directamente y lo hago store.dispatchcada vez que necesito, por ejemplo, entrar loadData?
Søren Debois
10
@ SørenDebois Si su aplicación es solo para el cliente, eso funcionaría. Si se procesa en el servidor, querrás tener una storeinstancia diferente para cada solicitud para que no puedas definirla de antemano.
Dan Abramov el
3
Solo quiero señalar que esta respuesta tiene 139 líneas, que es 9.92 veces más que el código fuente de redux-thunk que consta de 14 líneas: github.com/gaearon/redux-thunk/blob/master/src/index.js
Guy
447

Usted no

Pero ... deberías usar redux-saga :)

La respuesta de Dan Abramov es correcta, redux-thunkpero hablaré un poco más sobre redux-saga, que es bastante similar pero más poderosa.

Imperativo VS declarativo

  • DOM : jQuery es imperativo / React es declarativo
  • Mónadas : IO es imperativo / Free es declarativo
  • Efectos de Redux : redux-thunkes imperativo / redux-sagaes declarativo

Cuando tienes un golpe en tus manos, como una mónada de IO o una promesa, no puedes saber fácilmente qué hará una vez que ejecutes. La única forma de probar un thunk es ejecutarlo y burlarse del despachador (o de todo el mundo exterior si interactúa con más cosas ...).

Si está utilizando simulacros, entonces no está haciendo una programación funcional.

Visto a través de la lente de los efectos secundarios, los simulacros son una señal de que su código es impuro y, a los ojos del programador funcional, una prueba de que algo está mal. En lugar de descargar una biblioteca para ayudarnos a verificar que el iceberg esté intacto, deberíamos navegar por él. Un tipo duro de TDD / Java una vez me preguntó cómo te burlas en Clojure. La respuesta es, generalmente no lo hacemos. Generalmente lo vemos como una señal de que necesitamos refactorizar nuestro código.

Fuente

Las sagas (como se implementaron redux-saga) son declarativas y, al igual que la mónada libre o los componentes Reaccionar, son mucho más fáciles de probar sin ninguna simulación.

Ver también este artículo :

en la FP moderna, no deberíamos escribir programas, deberíamos escribir descripciones de los programas, que luego podemos introspectar, transformar e interpretar a voluntad.

(En realidad, Redux-saga es como un híbrido: el flujo es imperativo pero los efectos son declarativos)

Confusión: acciones / eventos / comandos ...

Hay mucha confusión en el mundo frontend sobre cómo algunos conceptos de backend como CQRS / EventSourcing y Flux / Redux pueden estar relacionados, principalmente porque en Flux usamos el término "acción" que a veces puede representar tanto el código imperativo ( LOAD_USER) como los eventos ( USER_LOADED) Creo que, al igual que el abastecimiento de eventos, solo debe enviar eventos.

Usando sagas en la práctica

Imagine una aplicación con un enlace a un perfil de usuario. La forma idiomática de manejar esto con cada middleware sería:

redux-thunk

<div onClick={e => dispatch(actions.loadUserProfile(123)}>Robert</div>

function loadUserProfile(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'USER_PROFILE_LOADED', data }),
      err => dispatch({ type: 'USER_PROFILE_LOAD_FAILED', err })
    );
}

redux-saga

<div onClick={e => dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert</div>


function* loadUserProfileOnNameClick() {
  yield* takeLatest("USER_NAME_CLICKED", fetchUser);
}

function* fetchUser(action) {
  try {
    const userProfile = yield fetch(`http://data.com/${action.payload.userId }`)
    yield put({ type: 'USER_PROFILE_LOADED', userProfile })
  } 
  catch(err) {
    yield put({ type: 'USER_PROFILE_LOAD_FAILED', err })
  }
}

Esta saga se traduce en:

cada vez que se hace clic en un nombre de usuario, busque el perfil del usuario y luego envíe un evento con el perfil cargado.

Como puede ver, hay algunas ventajas de redux-saga.

El uso de takeLatestpermisos para expresar que solo está interesado en obtener los datos del último nombre de usuario en el que se hizo clic (maneje los problemas de concurrencia en caso de que el usuario haga clic muy rápido en muchos nombres de usuario). Este tipo de cosas es difícil con los thunks. Podría haberlo usado takeEverysi no desea este comportamiento.

Mantienes a los creadores de acción puros. Tenga en cuenta que todavía es útil mantener actionCreators (en sagas puty componentes dispatch), ya que podría ayudarlo a agregar validación de acciones (aserciones / flujo / mecanografiado) en el futuro.

Su código se vuelve mucho más comprobable ya que los efectos son declarativos

Ya no necesita activar llamadas de tipo rpc como actions.loadUser(). Su interfaz de usuario solo necesita enviar lo que HA SUCEDIDO. Solo disparamos eventos (¡siempre en tiempo pasado!) Y ya no acciones. Esto significa que puede crear "patos" desacoplados o contextos limitados y que la saga puede actuar como el punto de acoplamiento entre estos componentes modulares.

Esto significa que sus vistas son más fáciles de administrar porque ya no necesitan contener esa capa de traducción entre lo que sucedió y lo que debería suceder como efecto

Por ejemplo, imagine una vista de desplazamiento infinita. CONTAINER_SCROLLEDpuede conducir a NEXT_PAGE_LOADED, pero ¿es realmente responsabilidad del contenedor desplazable decidir si debemos cargar o no otra página? Entonces, debe estar al tanto de cosas más complicadas, como si la última página se cargó correctamente o si ya hay una página que intenta cargarse, o si no quedan más elementos para cargar. No lo creo: para una reutilización máxima, el contenedor desplazable solo debe describir que se ha desplazado. La carga de una página es un "efecto comercial" de ese desplazamiento

Algunos podrían argumentar que los generadores pueden ocultar inherentemente el estado fuera de la tienda redux con variables locales, pero si comienzas a orquestar cosas complejas dentro de thunks iniciando temporizadores, etc., de todos modos, tendrías el mismo problema. Y hay un selectefecto que ahora permite obtener algún estado de su tienda Redux.

Las sagas pueden viajar en el tiempo y también permiten un registro de flujo complejo y herramientas de desarrollo en las que se está trabajando actualmente. Aquí hay algunos registros de flujo asíncrono simples que ya están implementados:

registro de flujo de saga

Desacoplamiento

Las sagas no solo están reemplazando los thunks redux. Vienen de backend / sistemas distribuidos / abastecimiento de eventos.

Es un error muy común pensar que las sagas están aquí para reemplazar sus redux thunks con una mejor capacidad de prueba. En realidad, esto es solo un detalle de implementación de redux-saga. El uso de efectos declarativos es mejor que los thunks para la comprobabilidad, pero el patrón de saga se puede implementar sobre el código imperativo o declarativo.

En primer lugar, la saga es una pieza de software que permite coordinar transacciones de larga duración (consistencia eventual) y transacciones en diferentes contextos delimitados (jerga de diseño dirigida por el dominio).

Para simplificar esto para el mundo frontend, imagine que hay widget1 y widget2. Cuando se hace clic en algún botón en widget1, entonces debería tener un efecto en widget2. En lugar de acoplar los 2 widgets (es decir, widget1 despacha una acción que se dirige a widget2), widget1 solo despacha que se hizo clic en su botón. Luego, la saga escuche este botón, haga clic y luego actualice widget2 presentando un nuevo evento que widget2 conoce.

Esto agrega un nivel de indirección que es innecesario para aplicaciones simples, pero hace que sea más fácil escalar aplicaciones complejas. Ahora puede publicar widget1 y widget2 en diferentes repositorios npm para que nunca tengan que conocerse, sin tener que compartir un registro global de acciones. Los 2 widgets ahora son contextos limitados que pueden vivir por separado. No se necesitan mutuamente para ser coherentes y también se pueden reutilizar en otras aplicaciones. La saga es el punto de acoplamiento entre los dos widgets que los coordinan de manera significativa para su negocio.

Algunos buenos artículos sobre cómo estructurar su aplicación Redux, en los que puede usar Redux-saga por motivos de desacoplamiento:

Un caso de uso concreto: sistema de notificación

Quiero que mis componentes puedan activar la visualización de notificaciones en la aplicación. Pero no quiero que mis componentes estén altamente acoplados al sistema de notificación que tiene sus propias reglas comerciales (máximo 3 notificaciones mostradas al mismo tiempo, cola de notificaciones, tiempo de visualización de 4 segundos, etc.).

No quiero que mis componentes JSX decidan cuándo se mostrará / ocultará una notificación. Solo le doy la posibilidad de solicitar una notificación y dejar las complejas reglas dentro de la saga. Este tipo de cosas es bastante difícil de implementar con thunks o promesas.

notificaciones

He descrito aquí cómo se puede hacer esto con la saga

¿Por qué se llama una saga?

El término saga proviene del mundo del backend. Inicialmente presenté a Yassine (el autor de Redux-saga) a ese término en una larga discusión .

Inicialmente, ese término se introdujo con un documento , se suponía que el patrón de la saga se usaría para manejar la consistencia eventual en las transacciones distribuidas, pero su uso se ha extendido a una definición más amplia por parte de los desarrolladores de back-end para que ahora también cubra el "administrador de procesos" patrón (de alguna manera el patrón de saga original es una forma especializada de administrador de procesos).

Hoy, el término "saga" es confuso ya que puede describir 2 cosas diferentes. Como se usa en redux-saga, no describe una forma de manejar transacciones distribuidas sino más bien una forma de coordinar acciones en su aplicación. redux-sagaTambién podría haber sido llamado redux-process-manager.

Ver también:

Alternativas

Si no le gusta la idea de usar generadores pero le interesa el patrón de saga y sus propiedades de desacoplamiento, también puede lograr lo mismo con redux-observable que usa el nombre epicpara describir exactamente el mismo patrón, pero con RxJS. Si ya está familiarizado con Rx, se sentirá como en casa.

const loadUserProfileOnNameClickEpic = action$ =>
  action$.ofType('USER_NAME_CLICKED')
    .switchMap(action =>
      Observable.ajax(`http://data.com/${action.payload.userId}`)
        .map(userProfile => ({
          type: 'USER_PROFILE_LOADED',
          userProfile
        }))
        .catch(err => Observable.of({
          type: 'USER_PROFILE_LOAD_FAILED',
          err
        }))
    );

Algunos recursos útiles de redux-saga

2017 aconseja

  • No abuses de Redux-saga solo por usarlo. Las llamadas de API comprobables solo no valen la pena.
  • No elimine los thunks de su proyecto para la mayoría de los casos simples.
  • No dude en enviar thunks yield put(someActionThunk)si tiene sentido.

Si tiene miedo de usar Redux-saga (o Redux-observable) pero solo necesita el patrón de desacoplamiento, verifique redux-dispatch-subscribe : permite escuchar despachos y desencadenar nuevos despachos en el oyente.

const unsubscribe = store.addDispatchListener(action => {
  if (action.type === 'ping') {
    store.dispatch({ type: 'pong' });
  }
});
Sebastien Lorber
fuente
64
Esto mejora cada vez que vuelvo a visitar. Considera convertirlo en una publicación de blog :).
RainerAtSpirit
44
Gracias por un buen artículo. Sin embargo, no estoy de acuerdo con ciertos aspectos. ¿Cómo es imprescindible LOAD_USER? Para mí, no solo es declarativo, sino que también ofrece un excelente código legible. Me gusta eg. "Cuando presiono este botón quiero ADD_ITEM". Puedo mirar el código y entender exactamente lo que está sucediendo. Si en cambio se llamara algo así como "BUTTON_CLICK", tendría que buscarlo.
swelet
44
Buena respuesta. Ahora hay otra alternativa: github.com/blesh/redux-observable
swennemen
44
@swelet lo siento por la respuesta tardía. Cuando despacha ADD_ITEM, es imprescindible porque despacha una acción que tiene como objetivo tener un efecto en su tienda: espera que la acción haga algo. Siendo declarativo, abrace la filosofía del abastecimiento de eventos: no despacha acciones para activar cambios en sus aplicaciones, sino que despacha eventos pasados ​​para describir lo que sucedió en su aplicación. El envío de un evento debería ser suficiente para considerar que el estado de la aplicación ha cambiado. El hecho de que haya una tienda Redux que reaccione al evento es un detalle de implementación opcional
Sebastien Lorber, del
3
No me gusta esta respuesta porque distrae de la pregunta real para comercializar la propia biblioteca de alguien. Esta respuesta proporciona una comparación de las dos bibliotecas, que no era la intención de la pregunta. La pregunta real es preguntar si usar middleware, lo cual se explica por la respuesta aceptada.
Abhinav Manchanda
31

La respuesta corta : me parece un enfoque totalmente razonable para el problema de la asincronía. Con un par de advertencias.

Tenía una línea de pensamiento muy similar cuando trabajaba en un nuevo proyecto que recién comenzamos en mi trabajo. Era un gran admirador del elegante sistema de Vanilla Redux para actualizar la tienda y volver a entregar los componentes de una manera que se mantiene fuera de las entrañas de un árbol de componentes React. Me pareció extraño conectarme a ese elegante dispatchmecanismo para manejar la asincronía.

Terminé con un enfoque realmente similar a lo que tienes allí en una biblioteca que factoré a partir de nuestro proyecto, que llamamos react-redux-controller .

Terminé sin seguir el enfoque exacto que tienes arriba por un par de razones:

  1. De la forma en que lo tiene escrito, esas funciones de despacho no tienen acceso a la tienda. Puede evitarlo haciendo que sus componentes de la interfaz de usuario pasen toda la información que necesita la función de envío. Pero diría que esto une esos componentes de la interfaz de usuario a la lógica de envío innecesariamente. Y lo que es más problemático, no hay una forma obvia para que la función de despacho acceda al estado actualizado en las continuaciones asíncronas.
  2. Las funciones de despacho tienen acceso a dispatchsí mismas a través del alcance léxico. Esto limita las opciones para refactorizar una vez que esa connectdeclaración se sale de control, y se ve bastante difícil de manejar con ese único updatemétodo. Por lo tanto, necesita algún sistema que le permita componer esas funciones de despachador si las divide en módulos separados.

En conjunto, debe armar algún sistema para permitir que dispatchla tienda se inyecte en sus funciones de despacho, junto con los parámetros del evento. Sé de tres enfoques razonables para esta inyección de dependencia:

  • redux-thunk hace esto de una manera funcional, pasándolos a sus thunks (haciéndolos no exactamente thunks en absoluto, según las definiciones de domo). No he trabajado con otros dispatchenfoques de middleware, pero supongo que son básicamente los mismos.
  • react-redux-controller hace esto con una rutina. Como beneficio adicional, también le da acceso a los "selectores", que son las funciones que puede haber pasado como primer argumento connect, en lugar de tener que trabajar directamente con la tienda sin procesar y normalizada.
  • También podría hacerlo orientado a objetos inyectándolos en el thiscontexto, a través de una variedad de mecanismos posibles.

Actualizar

Se me ocurre que parte de este enigma es una limitación de react-redux . El primer argumento para connectobtener una instantánea del estado, pero no el envío. El segundo argumento se despacha pero no el estado. Ninguno de los argumentos obtiene un thunk que se cierra sobre el estado actual, por poder ver el estado actualizado en el momento de una continuación / devolución de llamada.

acjay
fuente
22

El objetivo de Abramov, y todos lo ideal, es simplemente encapsular la complejidad (y las llamadas asíncronas) en el lugar donde sea más apropiado .

¿Dónde está el mejor lugar para hacer eso en el flujo de datos estándar de Redux? Qué tal si:

  • Reductores ? De ninguna manera. Deben ser funciones puras sin efectos secundarios. Actualizar la tienda es un asunto serio y complicado. No lo contamines.
  • ¿Componentes de vista tonta? Definitivamente No. Tienen una preocupación: presentación e interacción con el usuario, y deben ser lo más simples posible.
  • Componentes del contenedor? Posible, pero subóptimo. Tiene sentido porque el contenedor es un lugar donde encapsulamos cierta complejidad relacionada con la vista e interactuamos con la tienda, pero:
    • Los contenedores deben ser más complejos que los componentes tontos, pero sigue siendo una responsabilidad única: proporcionar enlaces entre la vista y el estado / tienda. Su lógica asíncrona es una preocupación completamente diferente de eso.
    • Al colocarlo en un contenedor, estaría bloqueando su lógica asíncrona en un solo contexto, para una sola vista / ruta. Mala idea. Idealmente, todo es reutilizable y está totalmente desacoplado.
  • ¿S u otro módulo de servicio? Mala idea: necesitarías inyectar acceso a la tienda, lo cual es una pesadilla de mantenibilidad / comprobabilidad. Es mejor seguir la corriente de Redux y acceder a la tienda solo usando las API / modelos provistos.
  • ¿Las acciones y los middlewares que las interpretan? ¡¿Por qué no?! Para empezar, es la única opción importante que nos queda. :-) Más lógicamente, el sistema de acción es una lógica de ejecución desacoplada que puede usar desde cualquier lugar. Tiene acceso a la tienda y puede enviar más acciones. Tiene una responsabilidad única que es organizar el flujo de control y datos alrededor de la aplicación, y la mayoría de los asíncronos encajan perfectamente en eso.
    • ¿Qué pasa con los creadores de acción? ¿Por qué no simplemente sincronizar allí, en lugar de hacerlo en las acciones mismas y en Middleware?
      • Primero y más importante, los creadores no tienen acceso a la tienda, como lo hace el middleware. Eso significa que no puede enviar nuevas acciones contingentes, no puede leer desde la tienda para componer su asíncrono, etc.
      • Por lo tanto, mantenga la complejidad en un lugar que sea complejo por necesidad y mantenga todo lo demás simple. Los creadores pueden ser funciones simples, relativamente puras y fáciles de probar.
XML
fuente
Componentes del contenedor : ¿por qué no? Debido al papel que desempeñan los componentes en React, un contenedor puede actuar como clase de servicio y ya obtiene una tienda a través de DI (accesorios). Al colocarlo en un contenedor, estaría bloqueando su lógica asíncrona en un solo contexto, para una sola vista / ruta , ¿cómo es eso? Un componente puede tener múltiples instancias. Se puede desacoplar de la presentación, por ejemplo, con render prop. Supongo que la respuesta podría beneficiarse aún más de ejemplos cortos que prueban el punto.
Estus Flas
¡Me encanta esta respuesta!
Mauricio Avendaño
13

Para responder la pregunta que se hace al principio:

¿Por qué el componente contenedor no puede llamar a la API asíncrona y luego enviar las acciones?

Tenga en cuenta que esos documentos son para Redux, no para Redux más React. Las tiendas Redux conectadas a los componentes React pueden hacer exactamente lo que usted dice, pero una tienda Plain Jane Redux sin middleware no acepta argumentos dispatchexcepto objetos simples.

Sin middleware, por supuesto, aún podría hacerlo

const store = createStore(reducer);
MyAPI.doThing().then(resp => store.dispatch(...));

Pero es un caso similar donde la asincronía se envuelve alrededor de Redux en lugar de ser manejada por Redux. Entonces, el middleware permite la asincronía modificando a qué se puede pasar directamente dispatch.


Dicho esto, el espíritu de su sugerencia es, creo, válido. Ciertamente, hay otras formas de manejar la asincronía en una aplicación Redux + React.

Una ventaja de usar middleware es que puedes seguir usando creadores de acción de forma normal sin preocuparte exactamente de cómo están conectados. Por ejemplo, usando redux-thunk, el código que escribió se parecería mucho a

function updateThing() {
  return dispatch => {
    dispatch({
      type: ActionTypes.STARTED_UPDATING
    });
    AsyncApi.getFieldValue()
      .then(result => dispatch({
        type: ActionTypes.UPDATED,
        payload: result
      }));
  }
}

const ConnectedApp = connect(
  (state) => { ...state },
  { update: updateThing }
)(App);

que no se ve tan diferente del original, simplemente se baraja un poco, y connectno sabe que updateThinges (o necesita ser) asíncrono.

Si también desea apoyar promesas , observables , sagas o creadores de acción personalizados y altamente declarativos , Redux puede hacerlo simplemente cambiando lo que le pasa dispatch(también conocido como lo que devuelve de los creadores de acción). No es necesario muckear con los componentes React (o connectllamadas).

Michelle Tilley
fuente
Aconsejas enviar otro evento más sobre la finalización de la acción. Eso no funcionará cuando necesite mostrar una alerta () después de completar la acción. Sin embargo, las promesas dentro de los componentes React sí funcionan. Actualmente recomiendo el enfoque de Promesas.
catamphetamine
8

OK, comencemos a ver cómo funciona el middleware primero, eso responde bastante a la pregunta, este es el código fuente de una función pplyMiddleWare en Redux:

function applyMiddleware() {
  for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
    middlewares[_key] = arguments[_key];
  }

  return function (createStore) {
    return function (reducer, preloadedState, enhancer) {
      var store = createStore(reducer, preloadedState, enhancer);
      var _dispatch = store.dispatch;
      var chain = [];

      var middlewareAPI = {
        getState: store.getState,
        dispatch: function dispatch(action) {
          return _dispatch(action);
        }
      };
      chain = middlewares.map(function (middleware) {
        return middleware(middlewareAPI);
      });
      _dispatch = compose.apply(undefined, chain)(store.dispatch);

      return _extends({}, store, {
        dispatch: _dispatch
      });
    };
  };
}

Mire esta parte, vea cómo nuestro despacho se convierte en una función .

  ...
  getState: store.getState,
  dispatch: function dispatch(action) {
  return _dispatch(action);
}
  • Tenga en cuenta que cada middleware recibirá las funciones dispatchy getStatecomo argumentos con nombre.

OK, así es como se presenta Redux-thunk como uno de los middlewares más utilizados para Redux:

El middleware Redux Thunk le permite escribir creadores de acciones que devuelven una función en lugar de una acción. El thunk se puede usar para retrasar el envío de una acción, o para enviar solo si se cumple una determinada condición. La función interna recibe el envío de métodos de almacenamiento y getState como parámetros.

Entonces, como puede ver, devolverá una función en lugar de una acción, lo que significa que puede esperar y llamarla cuando lo desee, ya que es una función ...

Entonces, ¿qué diablos es thunk? Así se introdujo en Wikipedia:

En la programación de computadoras, un thunk es una subrutina utilizada para inyectar un cálculo adicional en otra subrutina. Los thunks se usan principalmente para retrasar un cálculo hasta que sea necesario, o para insertar operaciones al principio o al final de la otra subrutina. Tienen una variedad de otras aplicaciones para compilar la generación de código y en la programación modular.

El término se originó como un derivado jocoso de "pensar".

Un thunk es una función que envuelve una expresión para retrasar su evaluación.

//calculation of 1 + 2 is immediate 
//x === 3 
let x = 1 + 2;

//calculation of 1 + 2 is delayed 
//foo can be called later to perform the calculation 
//foo is a thunk! 
let foo = () => 1 + 2;

Entonces, vea cuán fácil es el concepto y cómo puede ayudarlo a administrar sus acciones asíncronas ...

Eso es algo que puedes vivir sin él, pero recuerda que en la programación siempre hay formas mejores, más ordenadas y adecuadas de hacer las cosas ...

Aplicar middleware Redux

Alireza
fuente
1
Primera vez en SO, no leí nada. Pero me gustó la publicación que mira la imagen. Increíble, pista y recordatorio.
Bhojendra Rauniyar
2

Usar Redux-saga es el mejor middleware en la implementación de React-redux.

Ej: store.js

  import createSagaMiddleware from 'redux-saga';
  import { createStore, applyMiddleware } from 'redux';
  import allReducer from '../reducer/allReducer';
  import rootSaga from '../saga';

  const sagaMiddleware = createSagaMiddleware();
  const store = createStore(
     allReducer,
     applyMiddleware(sagaMiddleware)
   )

   sagaMiddleware.run(rootSaga);

 export default store;

Y luego saga.js

import {takeLatest,delay} from 'redux-saga';
import {call, put, take, select} from 'redux-saga/effects';
import { push } from 'react-router-redux';
import data from './data.json';

export function* updateLesson(){
   try{
       yield put({type:'INITIAL_DATA',payload:data}) // initial data from json
       yield* takeLatest('UPDATE_DETAIL',updateDetail) // listen to your action.js 
   }
   catch(e){
      console.log("error",e)
     }
  }

export function* updateDetail(action) {
  try{
       //To write store update details
   }  
    catch(e){
       console.log("error",e)
    } 
 }

export default function* rootSaga(){
    yield [
        updateLesson()
       ]
    }

Y luego action.js

 export default function updateFruit(props,fruit) {
    return (
       {
         type:"UPDATE_DETAIL",
         payload:fruit,
         props:props
       }
     )
  }

Y luego reducer.js

import {combineReducers} from 'redux';

const fetchInitialData = (state=[],action) => {
    switch(action.type){
      case "INITIAL_DATA":
          return ({type:action.type, payload:action.payload});
          break;
      }
     return state;
  }
 const updateDetailsData = (state=[],action) => {
    switch(action.type){
      case "INITIAL_DATA":
          return ({type:action.type, payload:action.payload});
          break;
      }
     return state;
  }
const allReducers =combineReducers({
   data:fetchInitialData,
   updateDetailsData
 })
export default allReducers; 

Y luego main.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './app/components/App.jsx';
import {Provider} from 'react-redux';
import store from './app/store';
import createRoutes from './app/routes';

const initialState = {};
const store = configureStore(initialState, browserHistory);

ReactDOM.render(
       <Provider store={store}>
          <App />  /*is your Component*/
       </Provider>, 
document.getElementById('app'));

prueba esto ... está funcionando

SM Chinna
fuente
3
Esto es algo serio para alguien que solo quiere llamar a un punto final API para devolver una entidad o lista de entidades. Usted recomienda, "solo haga esto ... luego esto, luego esto, luego esta otra cosa, luego eso, luego estas otras cosas, luego continúe, luego haga ...". Pero hombre, esto es FRONTEND, solo necesitamos llamar al BACKEND para darnos datos listos para usar en la interfaz. Si este es el camino a seguir, algo está mal, algo está realmente mal y alguien no está aplicando KISS hoy en día
zameb
Hola, use el bloque try and catch para las llamadas a la API. Una vez que la API haya dado la respuesta, llame a los tipos de acción Reductor.
SM Chinna
1
@zameb Puede que tenga razón, pero su queja, entonces, es con Redux, y todo lo que escucha al tratar de reducir la complejidad.
jorisw
1

Hay creadores de acciones sincrónicas y luego hay creadores de acciones asincrónicas.

Un creador de acciones sincrónicas es aquel que, cuando lo llamamos, devuelve inmediatamente un objeto de Acción con todos los datos relevantes adjuntos a ese objeto y está listo para ser procesado por nuestros reductores.

Los creadores de acciones asincrónicas requieren de un poco de tiempo antes de estar listos para enviar una acción.

Por definición, cada vez que tenga un creador de acciones que realice una solicitud de red, siempre calificará como creador de acciones asíncronas.

Si desea tener creadores de acciones asincrónicas dentro de una aplicación Redux, debe instalar algo llamado middleware que le permitirá tratar con esos creadores de acciones asincrónicas.

Puede verificar esto en el mensaje de error que nos dice que usemos middleware personalizado para acciones asíncronas.

Entonces, ¿qué es un middleware y por qué lo necesitamos para el flujo asíncrono en Redux?

En el contexto de middleware redux como redux-thunk, un middleware nos ayuda a tratar con creadores de acciones asíncronas, ya que eso es algo que Redux no puede manejar de inmediato.

Con un middleware integrado en el ciclo de Redux, todavía estamos llamando a los creadores de acciones, que devolverán una acción que se enviará, pero ahora cuando despachemos una acción, en lugar de enviarla directamente a todos nuestros reductores, vamos para decir que se enviará una acción a través de todos los middleware diferentes dentro de la aplicación.

Dentro de una sola aplicación Redux, podemos tener tantos o tan pocos middleware como queramos. En su mayor parte, en los proyectos en los que trabajamos tendremos uno o dos middleware conectados a nuestra tienda Redux.

Un middleware es una función simple de JavaScript que se llamará con cada acción que despachemos. Dentro de esa función, un middleware tiene la oportunidad de detener el envío de una acción a cualquiera de los reductores, puede modificar una acción o simplemente perder el tiempo con una acción de cualquier manera que, por ejemplo, podríamos crear un middleware que registre la consola cada acción que envíes solo para tu placer visual.

Hay una gran cantidad de middleware de código abierto que puede instalar como dependencias en su proyecto.

No está limitado a utilizar solo middleware de código abierto o instalarlos como dependencias. Puede escribir su propio middleware personalizado y usarlo dentro de su tienda Redux.

Uno de los usos más populares del middleware (y obtener su respuesta) es tratar con creadores de acciones asincrónicas, probablemente el middleware más popular que existe es redux-thunk y se trata de ayudarlo a lidiar con creadores de acciones asincrónicas.

Existen muchos otros tipos de middleware que también lo ayudan a tratar con creadores de acciones asincrónicas.

Daniel
fuente
1

Para responder la pregunta:

¿Por qué el componente contenedor no puede llamar a la API asíncrona y luego enviar las acciones?

Yo diría por al menos dos razones:

La primera razón es la separación de preocupaciones, no es el trabajo de action creatorllamar apiy recuperar datos, debe pasar dos argumentos a su action creator function, el action typey a payload.

La segunda razón es porque redux storeestá esperando un objeto simple con el tipo de acción obligatoria y opcionalmente un payload(pero aquí también tiene que pasar la carga útil).

El creador de la acción debe ser un objeto simple como el siguiente:

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

Y el trabajo de Redux-Thunk midlewareque dispacheel resultado de su api calla la apropiada action.

Mselmi Ali
fuente
0

Cuando se trabaja en un proyecto empresarial, hay muchos requisitos disponibles en middleware como (saga) no disponibles en flujo asíncrono simple, a continuación se detallan algunos:

  • Solicitud de ejecución en paralelo
  • Tirando acciones futuras sin la necesidad de esperar
  • Llamadas sin bloqueo Efecto de carrera, ejemplo de recogida primero
  • respuesta para iniciar el proceso Secuenciar sus tareas (primero en la primera llamada)
  • Composición
  • Cancelación de tareas Bifurcando dinámicamente la tarea.
  • Admite Concurrency Running Saga fuera del middleware redux.
  • Usando canales

La lista es larga, solo revise la sección avanzada en la documentación de la saga

Feras
fuente