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 showNotificationWithTimeout
sin 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 dispatch
como primer argumento? Porque necesita enviar acciones a la tienda. Normalmente, un componente tiene acceso, dispatch
pero 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 dispatch
directamente 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 dispatch
como 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 dispatch
una 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á dispatch
como 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 dispatch
como primer argumento. En su lugar, devuelve 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 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 dispatch
mé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 getState
como 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 dispatch
mé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.
redux-saga
respuesta 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