style attribute javascript timeout redux

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



title css (12)

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 sincrónico (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

Código

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

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 desde JSX. Prefiere agregar otra saga que escuche sus eventos de aplicaciones ya existentes, y luego TOAST_DISPLAY_REQUESTED : su componente que activa 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.

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 al usar setTimeout y devolver otra acción y no puedo encontrar cómo se hace esto en línea. Entonces cualquier consejo es bienvenido.


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.


¿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 setTimeout en 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.


Entiendo que esta pregunta es un poco antigua, 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!


Es simple. Use el paquete trim-redux y escriba así en componentDidMount otro lugar y mátelo componentWillUnmount .

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


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); }; }


Redux en sí mismo es una biblioteca bastante detallada, y para esas cosas tendría que usar algo como redux-thunk , que le dará una dispatch funció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.


Si desea manejar el tiempo de espera en acciones selectivas, puede probar el enfoque de middleware . 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 middleware


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 simplemente se pueden declarar como eliminar el rebote, la aceleración, la cancelación y solo utilizando 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 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 usar setTimeout . No hay ninguna razón por la cual las acciones de Redux sean diferentes.

Redux ofrece algunas formas alternativas de tratar 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 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 recibe un dispatch() o creadores de acciones específicas inyectados como accesorios. 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 despachar 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, enviará HIDE_NOTIFICATION , ocultando erróneamente la segunda notificación antes de que finalice 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 usar 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 el dispatch como primer argumento? Porque necesita enviar acciones a la tienda. Normalmente, un componente tiene acceso al 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 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 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 el 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 el dispatch como accesorio para que pueda pasarlo más. Ya no puedes vincular a los creadores de acción con connect() porque showNotificationWithTimeout() no es realmente un creador de acción. No devuelve una acción de Redux.

Además, puede ser incómodo recordar qué funciones son creadores de acciones sincrónicas como showNotification() y cuáles son ayudantes asincrónicos como 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 dispatch a 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 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 creador 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 el dispatch como primer argumento. En su lugar, devuelve una función que acepta el 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)

Llamamos al creador de la acción asíncrona para obtener la función interna que solo quiere dispatch , y luego pasamos el 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 método de dispatch 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 asíncrona (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 thunk 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 del dispatch , también pasa getState como el segundo argumento para 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 usa getState() solo 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 de dispatch() . Es por eso que puede devolver una Promesa desde un thunk y esperar a que se complete llamando a 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 método de dispatch 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 verbosas 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.



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í .