Saltar al contenido

¿Cómo enviar una acción de Redux con un tiempo de espera?

Después de de una extensa búsqueda de datos pudimos resolver este contratiempo que tienen ciertos los lectores. Te dejamos la solución y nuestro objetivo es servirte de mucha apoyo.

Solución:

No caiga en la trampa de pensar que una biblioteca debería prescribir cómo hacer todo. Si desea hacer algo con un tiempo de espera en JavaScript, debe usar setTimeout. No hay ninguna razón por la que las acciones de Redux deban ser diferentes.

Redux lo hace ofrecen algunas formas alternativas de lidiar con cosas asincrónicas, pero solo debe usarlas cuando se dé cuenta de que está repitiendo demasiado código. A menos que tenga este problema, use lo que ofrece el idioma y busque la solución más simple.

Escribir código asincrónico en línea

Esta es, con mucho, la forma más sencilla. Y aquí no hay nada específico de Redux.

store.dispatch( type: 'SHOW_NOTIFICATION', text: 'You logged in.' )
setTimeout(() => 
  store.dispatch( type: 'HIDE_NOTIFICATION' )
, 5000)

Del mismo modo, desde el interior de un componente conectado:

this.props.dispatch( type: 'SHOW_NOTIFICATION', text: 'You logged in.' )
setTimeout(() => 
  this.props.dispatch( type: 'HIDE_NOTIFICATION' )
, 5000)

La única diferencia es que en un componente conectado normalmente no tienes acceso a la tienda en sí, pero obtienes dispatch() o creadores de acciones específicas inyectados como accesorios. Sin embargo, esto no supone ninguna diferencia para nosotros.

Si no le gusta cometer errores tipográficos al enviar las mismas acciones desde diferentes componentes, es posible que desee extraer los creadores de acciones en lugar de enviar objetos de acción en línea:

// actions.js
export function showNotification(text) 
  return  type: 'SHOW_NOTIFICATION', text 

export function hideNotification() 
  return  type: 'HIDE_NOTIFICATION' 


// component.js
import  showNotification, hideNotification  from '../actions'

this.props.dispatch(showNotification('You just logged in.'))
setTimeout(() => 
  this.props.dispatch(hideNotification())
, 5000)

O, si los ha atado previamente con connect():

this.props.showNotification('You just logged in.')
setTimeout(() => 
  this.props.hideNotification()
, 5000)

Hasta ahora no hemos utilizado ningún middleware u otro concepto avanzado.

Extraer Creador de acciones asincrónicas

El enfoque anterior funciona bien en casos simples, pero es posible que tenga algunos problemas:

  • Te obliga a duplicar esta lógica en cualquier lugar donde quieras mostrar una notificación.
  • Las notificaciones no tienen ID, por lo que tendrá una condición de carrera si muestra dos notificaciones lo suficientemente rápido. Cuando finalice el primer tiempo de espera, se enviará HIDE_NOTIFICATION, ocultando por error la segunda notificación antes que después del tiempo de espera.

Para resolver estos problemas, necesitaría extraer una función que centralice la lógica del tiempo de espera y distribuya esas dos acciones. Podría verse así:

// actions.js
function showNotification(id, text) 
  return  type: 'SHOW_NOTIFICATION', id, text 

function hideNotification(id) 
  return  type: 'HIDE_NOTIFICATION', id 


let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) 
  // Assigning IDs to notifications lets reducer ignore HIDE_NOTIFICATION
  // for the notification that is not currently visible.
  // Alternatively, we could store the timeout ID and call
  // clearTimeout(), but we’d still want to do it in a single place.
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => 
    dispatch(hideNotification(id))
  , 5000)

Ahora los componentes pueden usar showNotificationWithTimeout sin duplicar esta lógica o tener condiciones de carrera con diferentes notificaciones:

// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')    

Por que showNotificationWithTimeout() aceptar dispatch como primer argumento? Porque necesita enviar acciones a la tienda. Normalmente, un componente tiene acceso a dispatch pero como queremos que una función externa tome el control del despacho, necesitamos darle control sobre el despacho.

Si tiene una tienda singleton exportada desde algún módulo, puede simplemente importarla y dispatch directamente sobre él en su lugar:

// store.js
export default createStore(reducer)

// actions.js
import store from './store'

// ...

let nextNotificationId = 0
export function showNotificationWithTimeout(text) 
  const id = nextNotificationId++
  store.dispatch(showNotification(id, text))

  setTimeout(() => 
    store.dispatch(hideNotification(id))
  , 5000)


// component.js
showNotificationWithTimeout('You just logged in.')

// otherComponent.js
showNotificationWithTimeout('You just logged out.')    

Esto parece más simple pero no recomendamos este enfoque. La principal razón por la que no nos gusta es porque obliga a la tienda a ser un singleton. Esto hace que sea muy difícil implementar la representación del servidor. En el servidor, querrá que cada solicitud tenga su propia tienda, de modo que diferentes usuarios obtengan diferentes datos precargados.

Una tienda singleton también dificulta las pruebas. Ya no puede burlarse de una tienda al probar los creadores de acciones porque hacen referencia a una tienda real específica exportada desde un módulo específico. Ni siquiera puedes restablecer su estado desde afuera.

Entonces, aunque técnicamente puede exportar una tienda singleton desde un módulo, lo desaconsejamos. No haga esto a menos que esté seguro de que su aplicación nunca agregará renderizado de servidor.

Volviendo a la versión anterior:

// actions.js

// ...

let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) 
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => 
    dispatch(hideNotification(id))
  , 5000)


// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')    

Esto resuelve los problemas de duplicación de la lógica y nos salva de las condiciones de carrera.

Middleware de procesador

Para aplicaciones simples, el enfoque debería ser suficiente. No se preocupe por el middleware si está satisfecho con él.

Sin embargo, en aplicaciones más grandes, es posible que encuentre ciertos inconvenientes a su alrededor.

Por ejemplo, parece lamentable que tengamos que pasar dispatch alrededor. Esto hace que sea más complicado separar el contenedor y los componentes de presentación porque cualquier componente que distribuya acciones de Redux de forma asincrónica de la manera anterior tiene que aceptar dispatch como un apoyo para que pueda pasarlo más lejos. No se puede simplemente vincular a los creadores de acciones con connect() más porque showNotificationWithTimeout() no es realmente un creador de acciones. No devuelve una acción de Redux.

Además, puede resultar incómodo recordar qué funciones son creadoras de acciones sincrónicas como showNotification() y que son ayudantes asincrónicos como showNotificationWithTimeout(). Tienes que usarlos de manera diferente y tener cuidado de no confundirlos entre sí.

Esta fue la motivación para encontrar una manera de “legitimar” este patrón de proporcionar dispatch a una función auxiliar, y ayudar a Redux a “ver” a los creadores de acciones sincrónicas como un caso especial de creadores de acciones normales en lugar de funciones totalmente diferentes.

Si todavía está con nosotros y también reconoce un problema en su aplicación, puede usar el middleware Redux Thunk.

En esencia, Redux Thunk le enseña a Redux a reconocer tipos especiales de acciones que son de hecho funciones:

import  createStore, applyMiddleware  from 'redux'
import thunk from 'redux-thunk'

const store = createStore(
  reducer,
  applyMiddleware(thunk)
)

// It still recognizes plain object actions
store.dispatch( type: 'INCREMENT' )

// But with thunk middleware, it also recognizes functions
store.dispatch(function (dispatch) 
  // ... which themselves may dispatch many times
  dispatch( type: 'INCREMENT' )
  dispatch( type: 'INCREMENT' )
  dispatch( type: 'INCREMENT' )

  setTimeout(() => 
    // ... even asynchronously!
    dispatch( type: 'DECREMENT' )
  , 1000)
)

Cuando este middleware está habilitado, si envías una función, El middleware Redux Thunk le dará dispatch como argumento. También se “tragará” tales acciones, así que no se preocupe si sus reductores reciben argumentos de función extraños. Sus reductores solo recibirán acciones de objetos simples, ya sea emitidas directamente o emitidas por las funciones como acabamos de describir.

Esto no parece muy útil, ¿verdad? No en esta situación particular. Sin embargo, nos deja declarar showNotificationWithTimeout() como creador habitual de acciones de Redux:

// actions.js
function showNotification(id, text) 
  return  type: 'SHOW_NOTIFICATION', id, text 

function hideNotification(id) 
  return  type: 'HIDE_NOTIFICATION', id 


let nextNotificationId = 0
export function showNotificationWithTimeout(text) 
  return function (dispatch) 
    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => 
      dispatch(hideNotification(id))
    , 5000)
  

Observe cómo la función es casi idéntica a la que escribimos en la sección anterior. Sin embargo, no acepta dispatch como primer argumento. En lugar de eso devoluciones una función que acepta dispatch como primer argumento.

¿Cómo lo usaríamos en nuestro componente? Definitivamente, podríamos escribir esto:

// component.js
showNotificationWithTimeout('You just logged in.')(this.props.dispatch)

Estamos llamando al creador de acciones asíncronas para obtener la función interna que solo quiere dispatch, y luego pasamos dispatch.

Sin embargo, ¡esto es aún más incómodo que la versión original! ¿Por qué fuimos por ese camino?

Por lo que te dije antes. Si el middleware Redux Thunk está habilitado, cada vez que intente distribuir una función en lugar de un objeto de acción, el middleware llamará a esa función con dispatch método en sí mismo como el primer argumento.

Entonces podemos hacer esto en su lugar:

// component.js
this.props.dispatch(showNotificationWithTimeout('You just logged in.'))

Finalmente, enviar una acción asincrónica (en realidad, una serie de acciones) no se ve diferente a enviar una sola acción de forma síncrona al componente. Lo cual es bueno porque a los componentes no debería importarles si algo sucede de forma sincrónica o asincrónica. Simplemente lo abstraemos.

Tenga en cuenta que, dado que “enseñamos” a Redux a reconocer tales creadores de acciones “especiales” (los llamamos creadores de acciones thunk), ahora podemos usarlos en cualquier lugar donde usaríamos creadores de acciones habituales. Por ejemplo, podemos usarlos con connect():

// actions.js

function showNotification(id, text) 
  return  type: 'SHOW_NOTIFICATION', id, text 

function hideNotification(id) 
  return  type: 'HIDE_NOTIFICATION', id 


let nextNotificationId = 0
export function showNotificationWithTimeout(text) 
  return function (dispatch) 
    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => 
      dispatch(hideNotification(id))
    , 5000)
  


// component.js

import  connect  from 'react-redux'

// ...

this.props.showNotificationWithTimeout('You just logged in.')

// ...

export default connect(
  mapStateToProps,
   showNotificationWithTimeout 
)(MyComponent)

Estado de lectura en Thunks

Por lo general, sus reductores contienen la lógica empresarial para determinar el siguiente estado. Sin embargo, los reductores solo se activan después de que se envían las acciones. ¿Qué sucede si tiene un efecto secundario (como llamar a una API) en un creador de acciones de procesador y desea evitarlo bajo alguna condición?

Sin usar el middleware thunk, simplemente haría esta verificación dentro del componente:

// component.js
if (this.props.areNotificationsEnabled) 
  showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

Sin embargo, el objetivo de extraer un creador de acciones era centralizar esta lógica repetitiva en muchos componentes. Afortunadamente, Redux Thunk le ofrece una forma de leer el estado actual de la tienda Redux. Además de dispatch, también pasa getState como segundo argumento de la función, regresa de su creador de acciones de procesador. Esto permite que el procesador lea el estado actual de la tienda.

let nextNotificationId = 0
export function showNotificationWithTimeout(text) 
  return function (dispatch, getState) 
    // Unlike in a regular action creator, we can exit early in a thunk
    // Redux doesn’t care about its return value (or lack of it)
    if (!getState().areNotificationsEnabled) 
      return
    

    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => 
      dispatch(hideNotification(id))
    , 5000)
  

No abuse de este patrón. Es bueno para rescatar las llamadas a la API cuando hay datos almacenados en caché disponibles, pero no es una base muy buena para construir su lógica comercial. Si utiliza getState() Solo para despachar condicionalmente diferentes acciones, considere colocar la lógica empresarial en los reductores.

Próximos pasos

Ahora que tiene una intuición básica sobre cómo funcionan los procesadores, consulte el ejemplo asíncrono de Redux que los usa.

Puede encontrar muchos ejemplos en los que los procesadores devuelven Promesas. Esto no es obligatorio pero puede resultar muy conveniente. A Redux no le importa lo que devuelva de un procesador, pero le da su valor de retorno de dispatch(). Es por eso que puede devolver una Promesa de un procesador y esperar a que se complete llamando dispatch(someThunkReturningPromise()).then(...).

También puede dividir los creadores de acciones de procesador complejas en varios creadores de acción de procesador más pequeños. los dispatch El método proporcionado por thunks puede aceptar thunks en sí mismo, por lo que puede aplicar el patrón de forma recursiva. Nuevamente, esto funciona mejor con Promises porque puede implementar un flujo de control asincrónico además de eso.

Para algunas aplicaciones, puede encontrarse en una situación en la que sus requisitos de flujo de control asincrónico son demasiado complejos para expresarse con procesadores. Por ejemplo, reintentar solicitudes fallidas, el flujo de reautorización con tokens o una incorporación paso a paso pueden ser demasiado detallados y propensos a errores cuando se escriben de esta manera. En este caso, es posible que desee buscar soluciones de flujo de control asíncrono más avanzadas, como Redux Saga o Redux Loop. Evalúelos, compare los ejemplos relevantes a sus necesidades y elija el que más le guste.

Por último, no utilice nada (incluidos los procesadores) si no los necesita. Recuerde que, según los requisitos, su solución puede parecer tan simple como

store.dispatch( type: 'SHOW_NOTIFICATION', text: 'You logged in.' )
setTimeout(() => 
  store.dispatch( type: 'HIDE_NOTIFICATION' )
, 5000)

No se preocupe a menos que sepa por qué está haciendo esto.

Usando Redux-saga

Como dijo Dan Abramov, si desea un control más avanzado sobre su código asincrónico, puede echar un vistazo a redux-saga.

Esta respuesta es un ejemplo simple, si desea mejores explicaciones sobre por qué redux-saga puede ser útil para su aplicación, consulte esta otra respuesta.

La idea general es que Redux-saga ofrece un intérprete de generadores ES6 que le permite escribir fácilmente código asíncrono que parece un código síncrono (es por eso que a menudo encontrará bucles while infinitos en Redux-saga). De alguna manera, Redux-saga está construyendo su propio lenguaje directamente dentro de Javascript. Redux-saga puede parecer un poco difícil de aprender al principio, porque necesita una comprensión básica de los generadores, pero también comprender el lenguaje que ofrece Redux-saga.

Intentaré describir aquí el sistema de notificación que construí sobre redux-saga. Este ejemplo se ejecuta actualmente en producción.

Especificación avanzada del sistema de notificación

  • Puede solicitar que se muestre una notificación
  • Puede solicitar una notificación para ocultar
  • Una notificación no debe mostrarse más de 4 segundos.
  • Se pueden mostrar varias notificaciones al mismo tiempo
  • No se pueden mostrar más de 3 notificaciones al mismo tiempo
  • Si se solicita una notificación mientras ya se muestran 3 notificaciones, colóquela en la cola / pospóngala.

Resultado

Captura de pantalla de mi aplicación de producción Stample.co

tostadas

Código

Aquí llamé a la notificación toast pero este es un detalle de nomenclatura.

function* toastSaga() 

    // Some config constants
    const MaxToasts = 3;
    const ToastDisplayTime = 4000;


    // Local generator state: you can put this state in Redux store
    // if it's really important to you, in my case it's not really
    let pendingToasts = []; // A queue of toasts waiting to be displayed
    let activeToasts = []; // Toasts currently displayed


    // Trigger the display of a toast for 4 seconds
    function* displayToast(toast) 
        if ( activeToasts.length >= MaxToasts ) 
            throw new Error("can't display more than " + MaxToasts + " at the same time");
        
        activeToasts = [...activeToasts,toast]; // Add to active toasts
        yield put(events.toastDisplayed(toast)); // Display the toast (put means dispatch)
        yield call(delay,ToastDisplayTime); // Wait 4 seconds
        yield put(events.toastHidden(toast)); // Hide the toast
        activeToasts = _.without(activeToasts,toast); // Remove from active toasts
    

    // Everytime we receive a toast display request, we put that request in the queue
    function* toastRequestsWatcher() 
        while ( true ) 
            // Take means the saga will block until TOAST_DISPLAY_REQUESTED action is dispatched
            const event = yield take(Names.TOAST_DISPLAY_REQUESTED);
            const newToast = event.data.toastData;
            pendingToasts = [...pendingToasts,newToast];
        
    


    // We try to read the queued toasts periodically and display a toast if it's a good time to do so...
    function* toastScheduler() 
        while ( true ) 
            const canDisplayToast = activeToasts.length < MaxToasts && pendingToasts.length > 0;
            if ( canDisplayToast ) 
                // We display the first pending toast of the queue
                const [firstToast,...remainingToasts] = pendingToasts;
                pendingToasts = remainingToasts;
                // Fork means we are creating a subprocess that will handle the display of a single toast
                yield fork(displayToast,firstToast);
                // Add little delay so that 2 concurrent toast requests aren't display at the same time
                yield call(delay,300);
            
            else 
                yield call(delay,50);
            
        
    

    // This toast saga is a composition of 2 smaller "sub-sagas" (we could also have used fork/spawn effects here, the difference is quite subtile: it depends if you want toastSaga to block)
    yield [
        call(toastRequestsWatcher),
        call(toastScheduler)
    ]

Y el reductor:

const reducer = (state = [],event) => 
    switch (event.name) 
        case Names.TOAST_DISPLAYED:
            return [...state,event.data.toastData];
        case Names.TOAST_HIDDEN:
            return _.without(state,event.data.toastData);
        default:
            return state;
    
;

Uso

Simplemente puede enviar TOAST_DISPLAY_REQUESTED eventos. Si envía 4 solicitudes, solo se mostrarán 3 notificaciones y la cuarta aparecerá un poco más tarde una vez que desaparezca la primera notificación.

Tenga en cuenta que no recomiendo específicamente enviar TOAST_DISPLAY_REQUESTED de JSX. Preferiría agregar otra saga que escuche los eventos de su aplicación ya existente y luego enviar el TOAST_DISPLAY_REQUESTED: su componente que activa la notificación, no tiene que estar estrechamente acoplado al sistema de notificación.

Conclusión

Mi código no es perfecto, pero se ejecuta en producción con 0 errores durante meses. Redux-saga y los generadores son un poco difíciles al principio, pero una vez que los entiendes, este tipo de sistema es bastante fácil de construir.

Incluso es bastante fácil implementar reglas más complejas, como:

  • cuando hay demasiadas notificaciones en “cola”, conceda menos tiempo de visualización para cada notificación para que el tamaño de la cola pueda disminuir más rápido.
  • detectar cambios en el tamaño de la ventana y cambiar el número máximo de notificaciones mostradas en consecuencia (por ejemplo, escritorio = 3, teléfono vertical = 2, teléfono horizontal = 1)

Honestamente, buena suerte implementando este tipo de cosas correctamente con thunks.

Tenga en cuenta que puede hacer exactamente el mismo tipo de cosas con redux-observable, que es muy similar a redux-saga. Es casi lo mismo y es cuestión de gustos entre generadores y RxJS.

Un repositorio con proyectos de muestra

Actualmente hay cuatro proyectos de muestra:

  1. Escribir código asincrónico en línea
  2. Extraer Creador de acciones asincrónicas
  3. Utilice Redux Thunk
  4. Utilice Redux Saga

La respuesta aceptada es asombrosa.

Pero falta algo:

  1. No hay proyectos de muestra ejecutables, solo algunos fragmentos de código.
  2. No hay código de muestra para otras alternativas, como:
    1. Saga Redux

Así que creé el repositorio Hello Async para agregar las cosas que faltan:

  1. Proyectos ejecutables. Puede descargarlos y ejecutarlos sin modificaciones.
  2. Proporcione un código de muestra para más alternativas:
    • Saga Redux
    • Bucle Redux

Saga Redux

La respuesta aceptada ya proporciona fragmentos de código de muestra para Async Code Inline, Async Action Generator y Redux Thunk. En aras de la integridad, proporciono fragmentos de código para Redux Saga:

// actions.js

export const showNotification = (id, text) => 
  return  type: 'SHOW_NOTIFICATION', id, text 


export const hideNotification = (id) => 
  return  type: 'HIDE_NOTIFICATION', id 


export const showNotificationWithTimeout = (text) => 
  return  type: 'SHOW_NOTIFICATION_WITH_TIMEOUT', text 

Las acciones son simples y puras.

// component.js

import  connect  from 'react-redux'

// ...

this.props.showNotificationWithTimeout('You just logged in.')

// ...

export default connect(
  mapStateToProps,
   showNotificationWithTimeout 
)(MyComponent)

Nada es especial con el componente.

// sagas.js

import  takeEvery, delay  from 'redux-saga'
import  put  from 'redux-saga/effects'
import  showNotification, hideNotification  from './actions'

// Worker saga
let nextNotificationId = 0
function* showNotificationWithTimeout (action) 
  const id = nextNotificationId++
  yield put(showNotification(id, action.text))
  yield delay(5000)
  yield put(hideNotification(id))


// Watcher saga, will invoke worker saga above upon action 'SHOW_NOTIFICATION_WITH_TIMEOUT'
function* notificationSaga () 
  yield takeEvery('SHOW_NOTIFICATION_WITH_TIMEOUT', showNotificationWithTimeout)


export default notificationSaga

Las sagas se basan en generadores ES6

// index.js

import createSagaMiddleware from 'redux-saga'
import saga from './sagas'

const sagaMiddleware = createSagaMiddleware()

const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)

sagaMiddleware.run(saga)

Comparado con Redux Thunk

Pros

  • No terminas en el infierno de devolución de llamada.
  • Puede probar sus flujos asincrónicos fácilmente.
  • Tus acciones se mantienen puras.

Contras

  • Depende de los generadores ES6, que es relativamente nuevo.

Consulte el proyecto ejecutable si los fragmentos de código anteriores no responden a todas sus preguntas.

Si para ti ha sido provechoso este artículo, sería de mucha ayuda si lo compartieras con otros programadores de este modo contrubuyes a extender nuestra información.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

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