ReactiveX Redux

Todos los que trabajan con Redux tarde o temprano se encontrarán con el problema de las acciones asincrónicas. Pero una aplicación moderna no se puede desarrollar sin ellos. Estas son solicitudes http para el backend y todo tipo de temporizadores / retrasos. Los propios creadores de Redux hablan sin ambigüedades: de forma predeterminada, solo se admite el flujo de datos sincrónico, todas las acciones asincrónicas deben colocarse en el middleware.

Por supuesto, esto es demasiado detallado e inconveniente, por lo que es difícil encontrar un desarrollador que use solo el middleware "nativo". Las bibliotecas y marcos como Thunk, Saga y similares siempre vienen al rescate.

Para la mayoría de las tareas, son suficientes. Pero, ¿qué pasa si se necesita una lógica un poco más compleja que enviar una solicitud o hacer un temporizador? Aquí hay un pequeño ejemplo:

async dispatch => { setTimeout(() => { try { await Promise .all([fetchOne, fetchTwo]) .then(([respOne, respTwo]) => { dispatch({ type: 'SUCCESS', respOne, respTwo }); }); } catch (error) { dispatch({ type: 'FAILED', error }); } }, 2000); } 

Es doloroso incluso mirar ese código, pero es simplemente imposible de mantener y expandir. ¿Qué hacer cuando se necesita un manejo de errores más sofisticado? ¿Qué pasa si necesita una solicitud repetida? ¿Y si quiero reutilizar esta función?

Mi nombre es Dmitry Samokhvalov, y en esta publicación les diré cuál es el concepto de Observable y cómo ponerlo en práctica junto con Redux, y también compararé todo esto con las capacidades de Redux-Saga.

Como regla, en tales casos, tome redux-saga. OK, reescribimos las sagas:

 try { yield call(delay, 2000); const [respOne, respTwo] = yield [ call(fetchOne), call(fetchTwo) ]; yield put({ type: 'SUCCESS', respOne, respTwo }); } catch (error) { yield put({ type: 'FAILED', error }); } 

Se ha vuelto notablemente mejor: el código es casi lineal, se ve y se lee mejor. Pero expandirse y reutilizarse sigue siendo difícil, porque la saga es tan imprescindible como el thunk.

Hay otro enfoque. Este es exactamente el enfoque, y no solo otra biblioteca para escribir código asincrónico. Se llama Rx (también son observables, corrientes reactivas, etc.). Lo usaremos y reescribiremos el ejemplo en Observable:

 action$ .delay(2000) .switchMap(() => Observable.merge(fetchOne, fetchTwo) .map(([respOne, respTwo]) => ({ type: 'SUCCESS', respOne, respTwo })) .catch(error => ({ type: 'FAILED', error })) 

El código no solo se volvió plano y disminuyó en volumen, sino que el principio mismo de describir acciones asincrónicas ha cambiado. Ahora no trabajamos directamente con consultas, sino que realizamos operaciones en objetos especiales llamados Observable.

Es conveniente representar Observable como una función que proporciona una secuencia (secuencia) de valores. Observable tiene tres estados principales: siguiente ("dar el siguiente valor"), error ("se produjo un error") y completo ("los valores han terminado, no hay nada más que dar"). En este sentido, es un poco como Promise, pero difiere en que es posible iterar sobre estos valores (y esta es una de las superpotencias observables). Puede envolver cualquier cosa en Observable: tiempos de espera, solicitudes http, eventos DOM, solo objetos js.



La segunda superpotencia observable son los operadores. Un operador es una función que acepta y devuelve un Observable, pero realiza alguna acción en la secuencia de valores. La analogía más cercana es el mapa y el filtro de JavaScript (por cierto, dichos operadores están en Rx).



Los más útiles para mí personalmente fueron los operadores zip, forkJoin y flatMap. Usando su ejemplo, es más fácil explicar el trabajo de los operadores.

El operador zip funciona de manera muy simple: toma unos pocos Observables (no más de 9) y devuelve en una matriz los valores que emiten.

 const first = fromEvent("mousedown"); const second = fromEvent("mouseup"); zip(first, second) .subscribe(e => console.log(`${e[0].x} ${e[1].x}`)); //output [119,120] [120,233] … 

En general, el trabajo de zip puede ser representado por el esquema:



Zip se usa si tiene varios Observables y necesita recibir constantemente valores de ellos (a pesar de que pueden emitirse a diferentes intervalos, sincrónicamente o no). Es muy útil cuando se trabaja con eventos DOM.

La instrucción forkJoin es similar a zip con una excepción: solo devuelve los últimos valores de cada Observable.



En consecuencia, es razonable usarlo cuando solo se necesitan valores finitos de la secuencia.
Un poco más complicado es el operador flatMap. Toma un Observable como entrada y devuelve un nuevo Observable, y asigna sus valores al nuevo Observable, utilizando una función de selector u otro Observable. Suena confuso, pero el diagrama es bastante simple:



Aún más claro en el código:

 const observable = of("Hello"); const promise = value => new Promise(resolve => resolve(`${value} World`); observable .flatMap(value => promise(value)) .subscribe(result => console.log(result)); //output "Hello World" 

Muy a menudo, flatMap se usa en solicitudes de back-end, junto con switchMap y concatMap.
¿Cómo puedo usar Rx en Redux? Hay una maravillosa biblioteca redux-observable para esto. Su arquitectura se ve así:



Todos los operadores y acciones observables en ellos se realizan en forma de middleware especial llamado épico. Cada epopeya toma acción como entrada, la envuelve en un Observable y debe devolver la acción, también como un Observable. No puede devolver una acción normal, esto crea un bucle sin fin. Escribamos una pequeña epopeya que hace una solicitud a la API.

 const fetchEpic = action$ => action$ .ofType('FETCH_INFO') .map(() => ({ type: 'FETCH_START' })) .flatMap(() => Observable .from(apiRequest) .map(data => ({ type: 'FETCH_SUCCESS', data })) .catch(error => ({ type: 'FETCH_ERROR', error })) ) 

Es imposible hacerlo sin comparar redux-observable y redux-saga. A muchos les parece que tienen una funcionalidad y capacidades cercanas, pero este no es el caso en absoluto. Las sagas son una herramienta completamente imprescindible, esencialmente un conjunto de métodos para trabajar con efectos secundarios. Observable es un estilo fundamentalmente diferente de escribir código asincrónico, si lo desea, una filosofía diferente.

Escribí varios ejemplos para ilustrar las posibilidades y el enfoque para resolver problemas.

Supongamos que necesitamos implementar un temporizador que se detendrá por acción. Así es como se ve en las sagas:

 while(true) { const timer = yield race({ stopped: take('STOP'), tick: call(wait, 1000) }) if (!timer.stopped) { yield put(actions.tick()) } else { break } } 

Ahora usa Rx:

 interval(1000) .takeUntil(action$.ofType('STOP')) 


Supongamos que hay una tarea para implementar una solicitud con cancelación en sagas:

 function* fetchSaga() { yield call(fetchUser); } while (yield take('FETCH')) { const fetchSaga = yield fork(fetchSaga); yield take('FETCH_CANCEL'); yield cancel(fetchSaga); } 

Todo es más simple en Rx:

 switchMap(() => fetchUser()) .takeUntil(action$.ofType('FETCH_CANCEL')) 

Finalmente mi favorito. Implemente una solicitud de API, en caso de falla, no realice más de 5 solicitudes repetidas con un retraso de 2 segundos. Esto es lo que tenemos en las sagas:

 for (let i = 0; i < 5; i++) { try { const apiResponse = yield call(apiRequest); return apiResponse; } catch (err) { if(i < 4) { yield delay(2000); } } } throw new Error(); } 

Lo que sucede en Rx:

 .retryWhen(errors => errors .delay(1000) .take(5)) 

Si resume los pros y los contras de la saga, obtendrá la siguiente imagen:



Las sagas son fáciles de aprender y muy populares, por lo que en la comunidad puedes encontrar recetas para casi todas las ocasiones. Desafortunadamente, el estilo imperativo impide el uso de las sagas de manera realmente flexible.

Rx tiene una situación completamente diferente:



Puede parecer que Rx es un martillo mágico y una bala de plata. Lamentablemente, esto no es así. El umbral para ingresar a Rx es mucho más alto, por lo tanto, es más difícil presentar a una persona nueva a un proyecto que usa Rx activamente.

Además, cuando se trabaja con Observable, es especialmente importante tener cuidado y siempre comprender bien lo que está sucediendo. De lo contrario, puede tropezar con errores no obvios o un comportamiento indefinido.

 action$ .ofType('DELETE') .switchMap(() => Observable .fromPromise(deleteRequest) .map(() => ({ type: 'DELETE_SUCCESS'}))) 

Una vez que escribí una epopeya que hizo un trabajo bastante simple, con cada acción del tipo 'BORRAR', se llamó a un método API que eliminó el elemento. Sin embargo, hubo problemas durante las pruebas. El probador se quejó de un comportamiento extraño: a veces, cuando hace clic en el botón Eliminar, no sucede nada. Resultó que el operador switchMap admite la ejecución de un solo Observable a la vez, un tipo de protección contra la condición de carrera.

Como resultado, daré algunas recomendaciones que sigo e instaré a todos los que comiencen a trabajar con Rx a seguir:

  • Ten cuidado
  • Revisa la documentación.
  • Revisa en la caja de arena.
  • Escribe pruebas.
  • No dispares gorriones desde el cañón.

Source: https://habr.com/ru/post/460155/


All Articles