javascript - react - ¿Cómo manejar los efectos secundarios complejos en Redux?
redux js framework (2)
Cuando desee dependencias asíncronas complejas, simplemente use Bacon, Rx, canales, sagas u otra abstracción asincrónica. Puedes usarlos con o sin Redux. Ejemplo con Redux:
observeSomething()
.flatMap(someTransformation)
.filter(someFilter)
.map(createActionSomehow)
.subscribe(store.dispatch);
Puede componer sus acciones asíncronas de la manera que desee; la única parte importante es que eventualmente se convierten en store.dispatch(action)
a store.dispatch(action)
.
Redux Thunk es suficiente para aplicaciones simples, pero a medida que sus necesidades de sincronización se vuelven más sofisticadas, necesita usar una abstracción de composición asíncrona real, y a Redux no le importa cuál use.
Actualización: Ha pasado algún tiempo y han surgido algunas soluciones nuevas. Le sugiero que consulte Redux Saga, que se ha convertido en una solución bastante popular para el flujo de control asíncrono en Redux.
He estado luchando durante horas para encontrar una solución a este problema ...
Estoy desarrollando un juego con un marcador en línea. El jugador puede iniciar y cerrar sesión en cualquier momento. Después de terminar un juego, el jugador verá el marcador y verá su propio rango, y el puntaje se enviará automáticamente.
El marcador muestra la clasificación del jugador y la clasificación.
El marcador se usa tanto cuando el usuario termina de jugar (para enviar un puntaje), y cuando el usuario solo quiere ver su clasificación.
Aquí es donde la lógica se vuelve muy complicada:
Si el usuario está conectado, el puntaje se enviará primero. Después de guardar el nuevo registro, se cargará el marcador.
De lo contrario, el marcador se cargará inmediatamente. El jugador tendrá la opción de iniciar sesión o registrarse. Después de eso, se enviará el puntaje, y luego el marcador se actualizará nuevamente.
Sin embargo, si no hay puntaje para enviar (simplemente viendo la tabla de puntaje más alto). En este caso, el registro existente del jugador simplemente se descarga. Pero dado que esta acción no afecta el marcador, tanto el marcador como el registro del jugador deben descargarse simultáneamente.
Hay un número ilimitado de niveles. Cada nivel tiene un marcador diferente. Cuando el usuario ve un marcador, el usuario está ''observando'' ese marcador. Cuando está cerrado, el usuario deja de observarlo.
El usuario puede iniciar y cerrar sesión en cualquier momento. Si el usuario cierra sesión, la clasificación del usuario debería desaparecer, y si el usuario inicia sesión como otra cuenta, entonces la información de clasificación para esa cuenta debe buscarse y mostrarse.
... pero esta búsqueda de esta información solo debería tener lugar para el marcador cuyo usuario está observando actualmente.
Para las operaciones de visualización, los resultados deben almacenarse en memoria caché, de modo que si el usuario se vuelve a suscribir al mismo marcador, no habrá recuperación. Sin embargo, si se envía un puntaje, no se debe usar el caché.
Cualquiera de estas operaciones de red puede fallar, y el jugador debe poder volver a intentarlas.
Estas operaciones deben ser atómicas. Todos los estados deben actualizarse de una vez (sin estados intermedios).
Actualmente, puedo resolver esto usando Bacon.js (una biblioteca de programación reactiva funcional), ya que viene con soporte de actualización atómica. El código es bastante conciso, pero en este momento es un código de spaghetti impredecible desordenado.
Empecé a mirar a Redux. Así que traté de estructurar la tienda, y se me ocurrió algo así (en sintaxis YAMLish):
user: (user information)
record:
level1:
status: (loading / completed / error)
data: (record data)
error: (error / null)
scoreboard:
level1:
status: (loading / completed / error)
data:
- (record data)
- (record data)
- (record data)
error: (error / null)
El problema es: ¿dónde pongo los efectos secundarios?
Para acciones sin efectos secundarios, esto se vuelve muy fácil. Por ejemplo, en la acción LOGOUT
, el reductor de record
podría simplemente apagar todos los registros.
Sin embargo, algunas acciones tienen un efecto secundario. Por ejemplo, si no estoy conectado antes de enviar el puntaje, entonces me SET_USER
éxito, la acción SET_USER
guarda al usuario en la tienda.
Pero debido a que tengo un puntaje para enviar, esta acción de SET_USER
también debe provocar el apagado de una solicitud de AJAX y, al mismo tiempo, establecer el record.levelN.status
para la loading
.
La pregunta es: ¿cómo quiero decir que los efectos secundarios (presentación de puntaje) deberían tener lugar cuando inicio sesión de forma atómica?
En la arquitectura de Elm, un actualizador también puede emitir efectos secundarios cuando usa la forma de Action -> Model -> (Model, Effects Action)
, pero en Redux, es simplemente (State, Action) -> State
.
Desde los documentos de Acciones Asincrónicas, la forma en que recomiendan es ponerlos en un creador de acciones. ¿Esto significa que la lógica de enviar el puntaje tendrá que incluirse en el creador de la acción para una acción de inicio de sesión exitosa también?
function login (options) {
return (dispatch) => {
service.login(options).then(user => dispatch(setUser(user)))
}
}
function setUser (user) {
return (dispatch, getState) => {
dispatch({ type: ''SET_USER'', user })
let scoreboards = getObservedScoreboards(getState())
for (let scoreboard of scoreboards) {
service.loadUserRanking(scoreboard.level)
}
}
}
Encuentro esto un poco extraño, porque el código responsable de esta reacción en cadena ahora existe en 2 lugares:
- En el reductor. Cuando se
SET_USER
acciónSET_USER
, el reductor derecord
también debe establecer el estado de los registros pertenecientes a los marcadores observados para laloading
. - En el creador de acciones, que realiza el efecto secundario real de obtener / enviar puntajes.
También parece que tengo que realizar un seguimiento manual de todos los observadores activos. Mientras que en la versión de Bacon.js, hice algo como esto:
Bacon.once() // When first observing the scoreboard
.merge(resubmit口) // When resubmitting because of network error
.merge(user川.changes().filter(user => !!user).first()) // When user logs in (but only once)
.flatMapLatest(submitOrGetRanking(data))
El código real de Bacon es mucho más largo, debido a todas las reglas complejas anteriores, que hicieron que la versión de Bacon apenas sea legible.
Pero Bacon realizó un seguimiento de todas las suscripciones activas de forma automática. Esto me llevó a empezar a cuestionar que tal vez no valga la pena el cambio, porque reescribir esto en Redux requeriría mucho manejo manual. ¿Alguien puede sugerir algún puntero?
Editar : ahora hay un proyecto de la saga redux inspirado en estas ideas
Aquí hay algunos buenos recursos
- Comparación de Redux-saga y Redux-thunk
- Redux-saga vs Redux-thunk con async / await
- Gestionando procesos en Redux Saga
- De acciones Creadores a Sagas
- Juego de serpiente implementado con Redux-saga
Flux / Redux se inspira en el procesamiento de la secuencia de eventos backend (cualquiera que sea el nombre: eventos, CQRS, CEP, arquitectura lambda ...).
Podemos comparar ActionCreators / Actions of Flux con Commands / Events (terminología usualmente utilizada en sistemas back-end).
En estas arquitecturas de back-end, utilizamos un patrón que a menudo se denomina Saga o Process Manager. Básicamente es una pieza en el sistema que recibe eventos, puede administrar su propio estado y luego puede emitir nuevos comandos. Para hacerlo simple, es un poco como implementar IFTTT (If-This-Then-That).
Puede implementar esto con FRP y BaconJS, pero también puede implementar esto sobre los reductores de Redux.
function submitScoreAfterLoginSaga(action, state = {}) {
switch (action.type) {
case SCORE_RECORDED:
// We record the score for later use if USER_LOGGED_IN is fired
state = Object.assign({}, state, {score: action.score}
case USER_LOGGED_IN:
if ( state.score ) {
// Trigger ActionCreator here to submit that score
dispatch(sendScore(state.score))
}
break;
}
return state;
}
Para dejarlo en claro: ¡el estado calculado a partir de los reductores para conducir las representaciones de React debería permanecer absolutamente puro! Pero no todo el estado de su aplicación tiene el propósito de desencadenar representaciones, y este es el caso aquí donde la necesidad es sincronizar diferentes partes desacopladas de su aplicación con reglas complejas. ¡El estado de esta "saga" no debería desencadenar una representación React!
No creo que Redux proporcione nada para apoyar este patrón, pero probablemente pueda implementarlo usted mismo fácilmente.
He hecho esto en nuestro marco de inicio y este patrón funciona bien. Nos permite manejar cosas como IFTTT:
Cuando la incorporación del usuario está activa y el usuario cierra Popup1, abra Popup2 y muestre alguna información sobre herramientas.
Cuando el usuario está usando un sitio web móvil y abre el Menú2, luego cierre el Menú1
IMPORTANTE : si está utilizando las funciones deshacer / rehacer / reproducir de algunos marcos como Redux, es importante que durante la reproducción de un registro de eventos, todas estas sagas no estén activadas, porque no quiere que se disparen nuevos eventos durante la reproducción. !