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
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:
- Escribir código asincrónico en línea
- Extraer Creador de acciones asincrónicas
- Utilice Redux Thunk
- Utilice Redux Saga
La respuesta aceptada es asombrosa.
Pero falta algo:
- No hay proyectos de muestra ejecutables, solo algunos fragmentos de código.
- No hay código de muestra para otras alternativas, como:
- Saga Redux
Así que creé el repositorio Hello Async para agregar las cosas que faltan:
- Proyectos ejecutables. Puede descargarlos y ejecutarlos sin modificaciones.
- 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.