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


891

Tengo una acción que actualiza el estado de notificación de mi aplicación. Por lo general, esta notificación será un error o información de algún tipo. Necesito luego enviar otra acción después de 5 segundos que devolverá el estado de notificación al inicial, por lo que no hay notificación. La razón principal detrás de esto es proporcionar funcionalidad donde las notificaciones desaparecen automáticamente después de 5 segundos.

No tuve suerte con el uso setTimeouty la devolución de otra acción y no puedo encontrar cómo se hace esto en línea. Entonces cualquier consejo es bienvenido.


30
No olvide verificar mi redux-sagarespuesta basada si desea algo mejor que thunks. Respuesta tardía, por lo que debe desplazarse mucho tiempo antes de que aparezca :) no significa que no valga la pena leerlo. Aquí hay un atajo: stackoverflow.com/a/38574266/82609
Sebastien Lorber

55
Siempre que configure setTimeout, no olvide borrar el temporizador usando clearTimeout en el método del ciclo de vida del componenteWillUnMount
Hemadri Dasari

2
redux-saga es genial, pero no parecen tener soporte para las respuestas escritas de las funciones del generador. Podría importar si estás usando el mecanografiado con react.
Crhistian Ramirez

Respuestas:


2619

No caigas 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 usarlo setTimeout. No hay ninguna razón por la cual las acciones de Redux sean diferentes.

Redux no ofrecen algunas opciones alternativas para tratar con cosas asíncrono, pero sólo se debe utilizar aquellas en las que se da cuenta que se 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 asíncrono en línea

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

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, generalmente no tiene acceso a la tienda en sí, sino que se dispatch()inyecta a los creadores de acciones específicas o a cualquiera de ellos . Sin embargo, esto no hace ninguna diferencia para nosotros.

Si no le gusta hacer errores tipográficos al despachar las mismas acciones desde diferentes componentes, es posible que desee extraer 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 vinculado 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.

Extrayendo Async Action Creator

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 finaliza el primer tiempo de espera, se despachará HIDE_NOTIFICATION, ocultando erróneamente la segunda notificación antes de que transcurra el 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 usarse showNotificationWithTimeoutsin duplicar esta lógica o tener condiciones de carrera con notificaciones diferentes:

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

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

¿Por qué showNotificationWithTimeout()acepta dispatchcomo primer argumento? Porque necesita enviar acciones a la tienda. Normalmente, un componente tiene acceso, dispatchpero dado que queremos que una función externa tome control sobre el despacho, debemos darle el control sobre el despacho.

Si tenía una tienda Singleton exportada desde algún módulo, simplemente podría importarla y dispatchdirectamente en ella:

// 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 razón principal 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, para que diferentes usuarios obtengan diferentes datos precargados.

Una tienda Singleton también hace que las pruebas sean más difíciles. Ya no puede burlarse de una tienda cuando prueba creadores de acciones porque hacen referencia a una tienda real específica exportada desde un módulo específico. Ni siquiera puede restablecer su estado desde el exterior.

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á la representación del 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 con la duplicación de la lógica y nos salva de las condiciones de carrera.

Thunk Middleware

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, puede encontrar ciertos inconvenientes a su alrededor.

Por ejemplo, parece desafortunado que tengamos que pasar dispatch. Esto hace que sea más difícil separar el contenedor y los componentes de presentación porque cualquier componente que despacha las acciones de Redux de forma asincrónica de la manera anterior tiene que aceptar dispatchcomo accesorio para que pueda pasarlo más. Ya no puedes vincular a los creadores de acción connect()porque showNotificationWithTimeout()realmente no es un creador de acción. No devuelve una acción de Redux.

Además, puede ser incómodo recordar qué funciones son como los creadores de acciones síncronas showNotification()y cuáles son como ayudantes asíncronos showNotificationWithTimeout(). Debe 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 dispatchuna función auxiliar y ayudar a Redux a "ver" a tales creadores de acciones asincró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 como un problema en su aplicación, puede usar el middleware Redux Thunk .

En resumen, Redux Thunk le enseña a Redux a reconocer tipos especiales de acciones que de hecho son 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 despacha una función , el middleware Redux Thunk lo dará dispatchcomo argumento. También "tragará" tales acciones, así que no se preocupe si sus reductores reciben argumentos de funciones extrañas. 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 permite declarar showNotificationWithTimeout()como creadores 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 dispatchcomo primer argumento. En su lugar, devuelve una función que acepta dispatchcomo 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 la acción asíncrona para obtener la función interna que quiere justamente dispatch, y luego aprobamos dispatch.

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

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

Entonces podemos hacer esto en su lugar:

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

Finalmente, despachar una acción asincrónica (realmente, una serie de acciones) no se ve diferente a despachar una sola acción sincrónicamente al componente. Lo cual es bueno porque los componentes no deberían preocuparse si algo sucede sincrónicamente o asincrónicamente. Acabamos de abstraer eso.

Tenga en cuenta que dado que "enseñamos" a Redux a reconocer a tales creadores de acción "especiales" (los llamamos creadores de acción thunk ), ahora podemos usarlos en cualquier lugar donde usaríamos creadores de acción regulares. 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 próximo 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 thunk y desea evitarlo bajo alguna condición?

Sin usar el middleware thunk, solo 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 punto de extraer un creador de acción 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 getStatecomo el segundo argumento a la función que devuelve de su creador de acción thunk. 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 abuses de este patrón. Es bueno para rescatar llamadas de API cuando hay datos disponibles en caché, pero no es una muy buena base para construir su lógica de negocios. Si getState()solo usa para despachar condicionalmente diferentes acciones, considere colocar la lógica de negocios en los reductores.

Próximos pasos

Ahora que tiene una intuición básica sobre cómo funcionan los thunks, consulte el ejemplo asincrónico de Redux que los usa.

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

También puede dividir creadores complejos de acción thunk en varios creadores más pequeños de acción thunk. El dispatchmétodo proporcionado por thunks puede aceptar thunks por 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 asíncrono además de eso.

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

Finalmente, no use nada (incluidos los thunks) si no los necesita realmente. 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.


27
Las acciones asíncronas parecen una solución tan simple y elegante para un problema común. ¿Por qué no se les brinda soporte para reducirlos sin la necesidad de middleware? Esta respuesta podría ser mucho más concisa.
Phil Mander

83
@PhilMander Porque hay muchos patrones alternativos como github.com/raisemarketplace/redux-loop o github.com/yelouafi/redux-saga que son igual de elegantes (si no más). Redux es una herramienta de bajo nivel. Puede construir un superconjunto que desee y distribuirlo por separado.
Dan Abramov

16
¿Puede explicar esto: * considere poner la lógica de negocios en los reductores *, eso significa que debo enviar una acción, y luego determinar en el reductor qué acciones adicionales enviar según mi estado? Mi pregunta es: ¿despacho otras acciones directamente en mi reductor y, de no ser así, desde dónde las envío?
froginvasion

25
Esta oración solo se aplica al caso síncrono. Por ejemplo, si escribe, if (cond) dispatch({ type: 'A' }) else dispatch({ type: 'B' })tal vez debería dispatch({ type: 'C', something: cond })y elija ignorar la acción en los reductores, dependiendo action.somethingdel estado actual.
Dan Abramov el

29
@DanAbramov Obtuviste mi voto a favor solo por esto "A menos que tengas este problema, usa lo que ofrece el idioma y busca la solución más simple". ¡Solo después me di cuenta de quién lo escribió!
Matt Lacey

189

Usando Redux-saga

Como dijo Dan Abramov, si desea un control más avanzado sobre su código asíncrono, 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 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 comprende 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
  • Puedes 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 hay 3 notificaciones mostradas, entonces póngala en cola / posponga.

Resultado

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

tostadas

Código

Aquí llamé a la notificación a toastpero 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_REQUESTEDeventos. 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_REQUESTEDdesde JSX. Prefiere agregar otra saga que escuche sus eventos de aplicaciones ya existentes, y luego enviar el TOAST_DISPLAY_REQUESTED: su componente que desencadena la notificación, no tiene que estar estrechamente vinculado 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 inicialmente, 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", dé menos tiempo de visualización para cada notificación, de modo 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)

Sinceramente, 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.


18
Desearía que tu respuesta llegara antes cuando se hizo la pregunta, porque no puedo estar más de acuerdo con el uso de la biblioteca de efectos secundarios de Saga para una lógica comercial como esta. Los reductores y los creadores de acción son para transiciones de estado. Los flujos de trabajo no son lo mismo que las funciones de transición de estado. Los flujos de trabajo atraviesan las transiciones, pero no son transiciones en sí mismas. Redux + React carece de esto por sí mismo; esta es exactamente la razón por la que Redux Saga es tan útil.
Atticus

44
Gracias, trato de hacer mi mejor esfuerzo para hacer que redux-saga sea popular por estas razones :) muy pocas personas piensan que actualmente redux-saga es solo un reemplazo para los thunks y no veo cómo redux-saga permite flujos de trabajo complejos y desacoplados
Sebastien Lorber

1
Exactamente. Las acciones y los reductores son parte de la máquina de estados. ¡A veces, para flujos de trabajo complejos, necesita algo más para orquestar la máquina de estado que no es directamente parte de la máquina de estado en sí!
Atticus

2
Acciones: cargas útiles / eventos al estado de transición. Reductores: funciones de transición de estado. Componentes: interfaces de usuario que reflejan el estado. Pero falta una pieza importante: ¿cómo maneja el proceso de muchas transiciones que tienen su propia lógica que determina qué transición realizar a continuación? Redux Saga!
Atticus

2
@mrbrdo si lee atentamente mi respuesta, notará que los tiempos de espera de notificación se manejan realmente yield call(delay,timeoutValue);: no es la misma API pero tiene el mismo efecto
Sebastien Lorber

25

Un repositorio con proyectos de muestra.

Actualmente hay cuatro proyectos de muestra:

  1. Escribir código asíncrono en línea
  2. Extrayendo Async Action Creator
  3. Utilice Redux Thunk
  4. Utiliza la saga Redux

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. Redux Saga

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

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

Redux Saga

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)

En comparación 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 todas sus preguntas.


23

Puedes hacer esto con redux-thunk . Hay una guía en el documento redux para acciones asíncronas como setTimeout.


Solo una pregunta de seguimiento rápida, cuando utilizo middleware applyMiddleware(ReduxPromise, thunk)(createStore)es así como agrega varios middleware (¿coma separado?) Ya que parece que no puedo hacer que funcione.
Ilja

1
@Ilja Esto debería funcionar:const store = createStore(reducer, applyMiddleware([ReduxPromise, thunk]));
geniuscarrier

22

También recomendaría echar un vistazo al patrón SAM .

El patrón SAM aboga por incluir un "predicado de la siguiente acción" donde las acciones (automáticas) como "las notificaciones desaparecen automáticamente después de 5 segundos" se activan una vez que el modelo se ha actualizado (modelo SAM ~ estado reductor + almacén).

El patrón aboga por la secuenciación de acciones y mutaciones del modelo una a la vez, porque el "estado de control" del modelo "controla" qué acciones están habilitadas y / o ejecutadas automáticamente por el predicado de la siguiente acción. Simplemente no puede predecir (en general) en qué estado estará el sistema antes de procesar una acción y, por lo tanto, si su próxima acción esperada será permitida / posible.

Entonces, por ejemplo, el código,

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

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

no se permitiría con SAM, porque el hecho de que se pueda enviar una acción hideNotification depende de que el modelo acepte con éxito el valor "showNotication: true". Podría haber otras partes del modelo que eviten que lo acepte y, por lo tanto, no habría razón para activar la acción hideNotification.

Recomiendo encarecidamente que implemente un predicado de próxima acción adecuado después de las actualizaciones de la tienda y se pueda conocer el nuevo estado de control del modelo. Esa es la forma más segura de implementar el comportamiento que está buscando.

Puedes unirte a nosotros en Gitter si quieres. También hay una guía de inicio de SAM disponible aquí .


Hasta ahora solo he arañado la superficie, pero ya estoy emocionado por el patrón SAM. V = S( vm( M.present( A(data) ) ), nap(M))es simplemente hermoso Gracias por compartir tus pensamientos y experiencias. Cavaré más profundo.

@ftor, gracias! Cuando lo escribí por primera vez, tuve el mismo sentimiento. He usado SAM en producción durante casi un año, y no puedo pensar en un momento en el que sintiera que necesitaba una biblioteca para implementar SAM (incluso vdom, aunque puedo ver cuándo podría usarse). Solo una línea de código, ¡eso es! SAM produce código isomorfo, no hay ambigüedad sobre cómo manejar las llamadas asíncronas ... No puedo pensar en un momento en el que, sin embargo, ¿qué estoy haciendo?
metaprogramador

SAM es un verdadero patrón de Ingeniería de Software (acaba de producir un SDK de Alexa con él). Se basa en TLA + e intenta llevar el poder de ese increíble trabajo a todos los desarrolladores. SAM corrige tres aproximaciones que (casi) todos han estado usando durante décadas: - las acciones pueden manipular el estado de la aplicación - las asignaciones son equivalentes a la mutación - no hay una definición precisa de qué es un paso de programación (por ejemplo, es un paso a = b * ca , son 1 / leer b, c 2 / calcular b * c, 3 / asignar a con el resultado tres pasos diferentes?
metaprogramador

20

Después de probar los diversos enfoques populares (creadores de acción, thunks, sagas, epopeyas, efectos, middleware personalizado), todavía sentía que tal vez había margen de mejora, así que documenté mi viaje en este artículo del blog, ¿Dónde pongo mi lógica empresarial? una aplicación React / Redux?

Al igual que las discusiones aquí, traté de contrastar y comparar los diversos enfoques. Finalmente, me llevó a presentar una nueva biblioteca redux-logic que se inspira en epopeyas, sagas, middleware personalizado.

Le permite interceptar acciones para validar, verificar, autorizar, así como proporcionar una forma de realizar IO asíncrona.

Algunas funcionalidades comunes se pueden declarar simplemente como eliminar el rebote, limitar, cancelar y solo usar la respuesta de la última solicitud (takeLatest). redux-logic envuelve su código proporcionando esta funcionalidad para usted.

Eso lo libera para implementar su lógica comercial central como quiera. No tiene que usar observables o generadores a menos que lo desee. Use funciones y devoluciones de llamada, promesas, funciones asíncronas (asíncrono / espera), etc.

El código para hacer una notificación 5s simple sería algo como:

const notificationHide = createLogic({
  // the action type that will trigger this logic
  type: 'NOTIFICATION_DISPLAY',
  
  // your business logic can be applied in several
  // execution hooks: validate, transform, process
  // We are defining our code in the process hook below
  // so it runs after the action hit reducers, hide 5s later
  process({ getState, action }, dispatch) {
    setTimeout(() => {
      dispatch({ type: 'NOTIFICATION_CLEAR' });
    }, 5000);
  }
});
    

Tengo un ejemplo de notificación más avanzado en mi repositorio que funciona de manera similar a lo que Sebastian Lorber describió, donde podría limitar la visualización a N elementos y rotar a través de cualquiera de los que estaban en cola. ejemplo de notificación de redux-logic

Tengo una variedad de ejemplos en vivo de redux-logic jsfiddle, así como ejemplos completos . Continúo trabajando en documentos y ejemplos.

Me encantaría escuchar tus comentarios.


¡No estoy seguro de que me guste su biblioteca, pero me gusta su artículo! ¡Hombre bien hecho! Has hecho suficiente trabajo para salvar el tiempo de los demás.
Tyler Long

2
Creé un proyecto de muestra para redux-logic aquí: github.com/tylerlong/hello-async/tree/master/redux-logic Creo que es un software bien diseñado y no veo ninguna desventaja importante en comparación con otros alternativas.
Tyler Long

9

Entiendo que esta pregunta es un poco vieja, pero voy a presentar otra solución usando redux-observable aka. Épico.

Citando la documentación oficial:

¿Qué es redux-observable?

Middleware basado en RxJS 5 para Redux. Componga y cancele acciones asíncronas para crear efectos secundarios y más.

Una epopeya es el núcleo primitivo de redux-observable.

Es una función que toma una secuencia de acciones y devuelve una secuencia de acciones. Acciones adentro, acciones afuera.

En más o menos palabras, puede crear una función que recibe acciones a través de una secuencia y luego devolver una nueva secuencia de acciones (utilizando efectos secundarios comunes como tiempos de espera, retrasos, intervalos y solicitudes).

Permítanme publicar el código y luego explicar un poco más al respecto

store.js

import {createStore, applyMiddleware} from 'redux'
import {createEpicMiddleware} from 'redux-observable'
import {Observable} from 'rxjs'
const NEW_NOTIFICATION = 'NEW_NOTIFICATION'
const QUIT_NOTIFICATION = 'QUIT_NOTIFICATION'
const NOTIFICATION_TIMEOUT = 2000

const initialState = ''
const rootReducer = (state = initialState, action) => {
  const {type, message} = action
  console.log(type)
  switch(type) {
    case NEW_NOTIFICATION:
      return message
    break
    case QUIT_NOTIFICATION:
      return initialState
    break
  }

  return state
}

const rootEpic = (action$) => {
  const incoming = action$.ofType(NEW_NOTIFICATION)
  const outgoing = incoming.switchMap((action) => {
    return Observable.of(quitNotification())
      .delay(NOTIFICATION_TIMEOUT)
      //.takeUntil(action$.ofType(NEW_NOTIFICATION))
  });

  return outgoing;
}

export function newNotification(message) {
  return ({type: NEW_NOTIFICATION, message})
}
export function quitNotification(message) {
  return ({type: QUIT_NOTIFICATION, message});
}

export const configureStore = () => createStore(
  rootReducer,
  applyMiddleware(createEpicMiddleware(rootEpic))
)

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {configureStore} from './store.js'
import {Provider} from 'react-redux'

const store = configureStore()

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

App.js

import React, { Component } from 'react';
import {connect} from 'react-redux'
import {newNotification} from './store.js'

class App extends Component {

  render() {
    return (
      <div className="App">
        {this.props.notificationExistance ? (<p>{this.props.notificationMessage}</p>) : ''}
        <button onClick={this.props.onNotificationRequest}>Click!</button>
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    notificationExistance : state.length > 0,
    notificationMessage : state
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onNotificationRequest: () => dispatch(newNotification(new Date().toDateString()))
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(App)

El código clave para resolver este problema es tan fácil como se puede ver, lo único que parece diferente de las otras respuestas es la función rootEpic.

Punto 1. Al igual que con las sagas, debe combinar las epopeyas para obtener una función de nivel superior que reciba una secuencia de acciones y devuelva una secuencia de acciones, para que pueda usarla con la fábrica de middleware createEpicMiddleware . En nuestro caso, solo necesitamos uno, así que solo tenemos nuestro rootEpic, por lo que no tenemos que combinar nada, pero es bueno saberlo.

Punto 2. Nuestro rootEpic que se ocupa de la lógica de los efectos secundarios solo toma alrededor de 5 líneas de código, ¡lo cual es increíble! ¡Incluyendo el hecho de que es bastante declarativo!

Punto 3. Línea por línea raíz Explicación épica (en comentarios)

const rootEpic = (action$) => {
  // sets the incoming constant as a stream 
  // of actions with  type NEW_NOTIFICATION
  const incoming = action$.ofType(NEW_NOTIFICATION)
  // Merges the "incoming" stream with the stream resulting for each call
  // This functionality is similar to flatMap (or Promise.all in some way)
  // It creates a new stream with the values of incoming and 
  // the resulting values of the stream generated by the function passed
  // but it stops the merge when incoming gets a new value SO!,
  // in result: no quitNotification action is set in the resulting stream
  // in case there is a new alert
  const outgoing = incoming.switchMap((action) => {
    // creates of observable with the value passed 
    // (a stream with only one node)
    return Observable.of(quitNotification())
      // it waits before sending the nodes 
      // from the Observable.of(...) statement
      .delay(NOTIFICATION_TIMEOUT)
  });
  // we return the resulting stream
  return outgoing;
}

¡Espero que ayude!


¿Podría explicar qué están haciendo los métodos de API específicos aquí, por ejemplo switchMap?
Dmitri Zaitsev

1
Estamos usando redux-observable en nuestra aplicación React Native en Windows. Es una solución de implementación elegante para un problema complejo y altamente asincrónico y tiene un soporte fantástico a través de su canal Gitter y problemas de GitHub. La capa adicional de complejidad solo vale la pena si llega al problema exacto que está destinado a resolver, por supuesto.
Matt Hargett

8

¿Por qué debería ser tan difícil? Es solo lógica de interfaz de usuario. Use una acción dedicada para establecer datos de notificación:

dispatch({ notificationData: { message: 'message', expire: +new Date() + 5*1000 } })

y un componente dedicado para mostrarlo:

const Notifications = ({ notificationData }) => {
    if(notificationData.expire > this.state.currentTime) {
      return <div>{notificationData.message}</div>
    } else return null;
}

En este caso, las preguntas deberían ser "¿cómo se limpia el estado anterior?", "Cómo notificar a un componente que la hora ha cambiado"

Puede implementar alguna acción TIMEOUT que se distribuye en setTimeout desde un componente.

Tal vez esté bien limpiarlo cada vez que se muestre una nueva notificación.

De todos modos, debería haber alguna setTimeouten algún lugar, ¿verdad? ¿Por qué no hacerlo en un componente?

setTimeout(() => this.setState({ currentTime: +new Date()}), 
           this.props.notificationData.expire-(+new Date()) )

La motivación es que la funcionalidad de "notificación se desvanece" es realmente una preocupación de la interfaz de usuario. Por lo tanto, simplifica las pruebas para la lógica de su negocio.

No parece tener sentido probar cómo se implementa. Solo tiene sentido verificar cuándo debe expirar la notificación. Por lo tanto, menos código para stub, pruebas más rápidas, código más limpio.


1
Esta debería ser la mejor respuesta.
mmla

6

Si desea manejar el tiempo de espera en acciones selectivas, puede probar el middleware enfoque de . Me enfrenté a un problema similar para manejar selectivamente las acciones basadas en promesas y esta solución fue más flexible.

Digamos que tu creador de acción se ve así:

//action creator
buildAction = (actionData) => ({
    ...actionData,
    timeout: 500
})

el tiempo de espera puede contener múltiples valores en la acción anterior

  • número en ms: durante un tiempo de espera específico
  • verdadero: para una duración de tiempo de espera constante. (manejado en el middleware)
  • indefinido - para despacho inmediato

Su implementación de middleware se vería así:

//timeoutMiddleware.js
const timeoutMiddleware = store => next => action => {

  //If your action doesn't have any timeout attribute, fallback to the default handler
  if(!action.timeout) {
    return next (action)
  }

  const defaultTimeoutDuration = 1000;
  const timeoutDuration = Number.isInteger(action.timeout) ? action.timeout || defaultTimeoutDuration;

//timeout here is called based on the duration defined in the action.
  setTimeout(() => {
    next (action)
  }, timeoutDuration)
}

Ahora puede enrutar todas sus acciones a través de esta capa de middleware usando redux.

createStore(reducer, applyMiddleware(timeoutMiddleware))

Puedes encontrar algunos ejemplos similares aquí


5

La forma adecuada de hacer esto es usar Redux Thunk, que es un middleware popular para Redux, según la documentación de Redux Thunk:

"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 los métodos de almacenamiento dispatch y getState como parámetros ".

Básicamente, devuelve una función, y puede retrasar su envío o ponerlo en un estado de condición.

Entonces, algo como esto hará el trabajo por usted:

import ReduxThunk from 'redux-thunk';

const INCREMENT_COUNTER = 'INCREMENT_COUNTER';

function increment() {
  return {
    type: INCREMENT_COUNTER
  };
}

function incrementAsync() {
  return dispatch => {
    setTimeout(() => {
      // Yay! Can invoke sync or async actions with `dispatch`
      dispatch(increment());
    }, 5000);
  };
}

4

Es simple. Usa el paquete trim-redux y escribe así en componentDidMountotro lugar y mátalo componentWillUnmount.

componentDidMount() {
  this.tm = setTimeout(function() {
    setStore({ age: 20 });
  }, 3000);
}

componentWillUnmount() {
  clearTimeout(this.tm);
}

3

Redux en sí mismo es una biblioteca bastante detallada, y para tales cosas tendría que usar algo como Redux-thunk , que le dará una dispatchfunción, por lo que podrá enviar el cierre de la notificación después de varios segundos.

He creado una biblioteca para abordar problemas como la verbosidad y la componibilidad, y su ejemplo se verá así:

import { createTile, createSyncTile } from 'redux-tiles';
import { sleep } from 'delounce';

const notifications = createSyncTile({
  type: ['ui', 'notifications'],
  fn: ({ params }) => params.data,
  // to have only one tile for all notifications
  nesting: ({ type }) => [type],
});

const notificationsManager = createTile({
  type: ['ui', 'notificationManager'],
  fn: ({ params, dispatch, actions }) => {
    dispatch(actions.ui.notifications({ type: params.type, data: params.data }));
    await sleep(params.timeout || 5000);
    dispatch(actions.ui.notifications({ type: params.type, data: null }));
    return { closed: true };
  },
  nesting: ({ type }) => [type],
});

Por lo tanto, redactamos acciones de sincronización para mostrar notificaciones dentro de la acción asíncrona, que puede solicitar información de fondo o verificar más adelante si la notificación se cerró manualmente.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.