Buen dia
¿También tiene un desarrollador de reacción familiar que cuenta historias sorprendentes sobre los efectos secundarios en redux? No? ¿Puedo ser esta persona?

El autor se tomó la libertad de no escribir una parte introductoria sobre de qué se trata la biblioteca de la saga redux. Espera que en caso de datos insuficientes, un lector generoso use la búsqueda de Habr o el tutorial oficial . Estos ejemplos se simplifican enormemente para transmitir la esencia.
Entonces, ¿por qué los puse a todos juntos? Se tratará del uso de la saga redux en los espacios abiertos de los clientes de combate. Más específicamente, sobre casos más complejos e interesantes que "aceptar acción => enviar solicitud de API => crear una nueva acción".
Espero estimular un estudio más profundo de esta biblioteca por parte de los conciudadanos, así como compartir el placer de cómo las cosas asincrónicas sofisticadas se vuelven más comprensibles y expresivas.
Websockets
Caso de uso: reciba actualizaciones de la lista de vacantes disponibles del servidor en tiempo real utilizando el modelo push.
Se trata, por supuesto, del uso de sockets web. Por ejemplo, tome socket.io, pero de hecho, la API de socket no importa aquí.
En las sagas existe un canal. Este es un bus de mensajes a través del cual la fuente de eventos puede comunicarse con su consumidor. El objetivo principal de los canales es la comunicación entre sagas y la conversión del flujo de eventos asincrónicos en una estructura conveniente para el trabajo.
Por defecto, store es el canal principal de eventos para la saga redux. Los eventos vienen en forma de acción. Los canales se utilizan para trabajar con eventos que no son de la tienda.
Resulta que el canal es justo lo que necesita para trabajar con la secuencia asíncrona de mensajes desde el socket. ¡Creemos el canal lo antes posible!
Pero primero, crea un socket:
import io from 'socket.io-client'; export const socket = io.connect('/');
Ahora declara una modesta lista de eventos:
export const SocketEvents = { jobsFresh: 'jobs+fresh', };
El siguiente es un método de fábrica para crear un canal. El código crea un método para suscribirse a eventos que nos interesan desde el socket, un método para cancelar la suscripción y, directamente, el canal de eventos en sí:
import { eventChannel } from 'redux-saga'; import { socket } from '../apis/socket'; import { SocketEvents } from '../constants/socket-events'; export function createFreshJobsChannel() { const subscribe = emitter => { socket.on(SocketEvents.jobsFresh, emitter); return () => socket.removeListener(SocketEvents.jobsFresh, emitter); }; return eventChannel(subscribe); }
Escribamos una saga bastante simple, esperando actualizaciones del socket y convirtiéndolas a la acción correspondiente:
import { take, call, put } from 'redux-saga/effects'; import { createFreshJobsChannel } from '../channels/fresh-jobs'; import { JobsActions } from '../actions/jobs'; export function * freshJobsSaga() { const channel = yield call(createFreshJobsChannel); while (true) { const jobs = yield take(channel); const action = JobsActions.fresh(jobs); yield put(action); } }
Solo queda vincularlo a la saga raíz:
import { fork } from 'redux-saga/effects'; import { freshJobsSaga } from './fresh-jobs'; export function * sagas() { yield fork(freshJobsSaga); }
Autocompletar de Google Places
Caso de uso: muestre sugerencias cuando el usuario ingrese una ubicación geográfica para la búsqueda posterior de bienes inmuebles cercanos.
De hecho, necesitamos las coordenadas, y el usuario necesita el nombre legible por humanos del área deseada.
Parece que esta tarea difiere de la aburrida "acción => API => acción"? En el caso de la finalización automática, queremos hacer la menor cantidad posible de llamadas inútiles a recursos externos, y también mostrarle al usuario solo sugerencias relevantes.
Primero, escribiremos un método API utilizando el Servicio de autocompletado de Google Places. Una de las cosas interesantes aquí es la limitación de las indicaciones dentro de un país determinado:
export function getPlaceSuggestions(autocompleteService, countryCode, query) { return new Promise(resolve => { autocompleteService.getPlacePredictions({ componentRestrictions: { country: countryCode }, input: query, }, resolve); }); }
Hay un método API que utilizaremos, puede comenzar a escribir una saga. Es hora de aclarar las solicitudes inútiles.
La implementación en la frente, cuando el usuario escribe, y para cada cambio, leer, para cada carácter, enviar una solicitud a la API, no lleva a ninguna parte. Mientras el usuario escribe, no necesita sugerencias. Pero cuando se detiene, es hora de servirlo.
Además, no querríamos estar en una situación en la que el usuario escribió algo, se detuvo, la solicitud de API desapareció, el usuario escribió algo, otra solicitud desapareció.
Por lo tanto, creamos una carrera entre dos solicitudes. Los eventos pueden desarrollarse de dos maneras y ambas no son muy agradables.
Por ejemplo, una solicitud irrelevante finalizará antes que la actual y, por un momento, el usuario verá mensajes irrelevantes. Desagradable, pero no crítico.
O bien, la solicitud actual finaliza antes que la irrelevante y, después de parpadear, el usuario permanecerá con las mismas indicaciones irrelevantes. Esto ya es crítico.
Por supuesto, no somos los primeros en encontrar un problema de este tipo y la técnica conocida como rebote nos ayudará: la ejecución de la tarea solo después de N unidades de tiempo desde que se recibió el último evento. Aquí hay un poco de material sobre esto.
En la saga redux, esta técnica se implementa utilizando dos efectos: delay y takeLatest . El primero pospone la ejecución de la saga por el número especificado de milisegundos. El segundo interrumpe la ejecución de una saga que ya funciona cuando llega un nuevo evento.
Sabiendo todo esto, escribiremos una saga:
import { delay } from 'redux-saga'; import { put, call, select } from 'redux-saga/effects'; import { PlaceActions } from '../actions/place'; import { MapsActions } from '../actions/maps'; import { getPlaceSuggestions } from '../api/get-place-suggestions'; export function placeSuggestionsSaga * ({ payload: query }) { const { maps: { isApiLoaded } } = yield select();
Como en el ejemplo anterior, solo queda vincularlo a la saga raíz:
import { takeLatest } from 'redux-saga/effects'; import { PlaceActions } from '../actions/place'; import { placeSuggestionsSaga } from './place-suggestions'; export function * sagas() { yield takeLatest( PlaceActions.changeQuery, placeSuggestionsSaga, ); }
Desplegables más cerca
Caso de uso: cierre las listas desplegables autoescritas al hacer clic fuera del territorio del control.
De hecho, esta es una emulación del comportamiento integrado en la selección del navegador. Las razones por las que podría necesitar una lista desplegable escrita en divs se dejarán para la imaginación del lector.
Una característica clave de la tarea que se está resolviendo es el paso de un evento fuera del control, por ejemplo, al hacer clic fuera de la lista.
Adivinado? Sí, los canales también nos ayudarán aquí. Utilizándolos, convertiremos los eventos de clic que aparecen en la parte superior en la acción correspondiente.
Sería bueno tener un método de fábrica que cree canales para un evento de ventana arbitrario. Y aquí está él:
import { eventChannel } from 'redux-saga'; export function createWindowEventChannel(eventName) { const subscribe = emitter => { window.addEventListener(eventName, emitter); return () => window.removeEventListener(eventName, emitter); }; return eventChannel(subscribe); }
Creamos una saga muy similar al primer ejemplo (puede crear un método de fábrica para ellos si lo desea):
import { take, put, call } from 'redux-saga/effects'; import { createWindowEventChannel } from '../channels/window-event'; import { DropdownActions } from '../actions/dropdown'; export function * closeDropdownsSaga() { const channel = yield call(createWindowEventChannel, 'onClick'); while (true) { const event = yield take(channel); const action = DropdownActions.closeAll(event); yield put(action(event)); } }
Los reductores interesados pondrán el control en un estado cerrado:
import { handleActions } from 'redux-actions'; import { DropdownActions } from '../actions/dropdown'; export const priceReducer = handleActions({ ..., [DropdownActions.closeAll]: state => ({ ...state, isOpen: false}), }, {});
La lista desplegable en sí debe detener la propagación del evento de clic en cualquier parte interna y enviar el evento de cierre a la tienda por sí solo. Por ejemplo, cuando hace clic para abrir:
De lo contrario, la lista simplemente no se abrirá. Lo mismo se aplica a un clic al seleccionar una opción.
El clasico, monta la saga:
import { fork } from 'redux-saga/effects'; import { closeDropdownsSaga } from './close-dropdowns'; export function * sagas() { yield fork(closeDropdownsSaga); }
Notificaciones
Caso de uso: muestra notificaciones del navegador sobre la disponibilidad de nuevas vacantes, si la pestaña está en segundo plano.
En la pestaña activa, el usuario verá el cambio en un control especial y, por lo tanto, las notificaciones no son apropiadas. Pero para la pestaña de fondo puede ser útil. Por supuesto, con el permiso del usuario!
También me gustaría hacer clic en la notificación para ir a la pestaña y mostrar nuevas vacantes. Si el usuario no responde, cierre la notificación. Para hacer esto, necesitamos otro efecto útil: la raza . Te permite organizar una carrera entre varios otros efectos. En la mayoría de los casos, la carrera se utiliza para proporcionar un tiempo de espera para alguna operación.
Omitimos el código para rastrear la actividad de la pestaña, debido a la identidad con el código de intercepción de clics del ejemplo anterior.
Escribiremos un método de fábrica que creará un canal para solicitar la aprobación de un usuario para recibir notificaciones:
import { eventChannel, END } from 'redux-saga'; export function createRequestNotificationPermissionChannel() { const subscribe = emitter => { Notification.requestPermission(permission => { emitter(permission); emitter(END); }); return () => {}; }; return eventChannel(subscribe); }
En busca de otro método de fábrica, pero con un canal para recibir una notificación, haga clic en:
import { eventChannel, END } from 'redux-saga'; export function createNotificationClickChannel(notification) { const subscribe = emitter => { notification.onclick = event => { emitter(event); emitter(END); }; return () => notification.onclick = null; }; return eventChannel(subscribe); }
Ambos canales son de un solo uso y máximo en un evento, después de lo cual se cierran.
La clave permanece: la saga con lógica. Compruebe si la pestaña está activa, solicite permiso, cree una notificación, espere un clic o un tiempo de espera, muestre nuevas vacantes, active la pestaña y luego cierre la notificación:
import { delay } from 'redux-saga'; import { call, select, race, take } from 'redux-saga/effects'; import { createRequestNotificationPermissionChannel } from '../channels/request-notification-permission'; import { createNotificationClickChannel } from '../channels/notification-click'; import { JobsActions } from '../actions/jobs'; export function * notificationsSaga(action) { const { inFocus } = yield select(); if (inFocus) return; const permissionChannel = yield call(createRequestNotificationPermissionChannel); const permission = yield take(permissionChannel); if (permission !== 'granted') return; const notification = new Notification( `You have ${action.payload.jobs.length} new job posts`, { icon: 'assets/new-jobs.png' } ); const clickChannel = yield call(createNotificationClickChannel, notification); const { click, timeout } = yield race({ click: take(clickChannel), timeout: call(delay, 5000), }); if (click) { yield put(JobsActions.show()); window.focus(); window.scrollTo(0, 0); } notification.close(); }
Monta la saga antes de hacer esta detección de características:
import { takeEvery } from 'redux-saga/effects'; import { JobsActions } from '../actions/jobs'; import { notificationsSaga } from './notifications'; export default function * sagas() { if ( 'Notification' in window && Notification.permission !== 'denied' ) { yield takeEvery(JobsActions.fresh, notificationsSaga); } }
Bus de eventos globales
Caso de uso: transfiere la categoría de eventos especificada entre redux-stores.
Tal bus es necesario si varias aplicaciones con datos comunes están presentes en la página. Al mismo tiempo, las aplicaciones pueden implementarse independientemente una de la otra.
Por ejemplo, una cadena de búsqueda con filtros y resultados de búsqueda como aplicaciones de reacción separadas. Al cambiar los filtros, quiero que la aplicación de resultados lo sepa si también está en la página.
Utilizamos el emisor de eventos estándar:
import EventEmmiter from 'events'; if (!window.GlobalEventBus) { window.GlobalEventBus = new EventEmmiter(); } export const globalEventBus = window.GlobalEventBus;
El ya querido eventChannel convierte un emisor estándar en un canal:
import { eventChannel } from 'redux-saga'; import { globalEventBus as bus } from '../utils/global-event-bus'; exports function createGlobalEventBusChannel() { const subscribe = emitter => { const handler = event => emitter({ ...event, external: true }); bus.on('global.event', handler); return bus.removeListener('global.event', handler); }; return eventChannel(subscribe); }
La saga es bastante simple: creamos un canal y aceptamos interminablemente eventos, ya sean internos o externos. Si recibimos un evento interno, envíelo al bus, si es externo, en la tienda:
import { take, put, race, call } from 'redux-saga/effects'; import { globalEventBus as bus } from '../utils/global-event-bus'; import { createGlobalEventBusChannel } from '../channels/global-event-bus'; export function * globalEventBusSaga(allowedActions) { allowedActions = allowedActions.map(x => x.toString()); const channel = yield call(createGlobalEventBusChannel); while (true) { const { local, external } = yield race({ local: take(), external: take(channel), }); if ( external && allowedActions.some(action => action === external.type) ) { yield put(external); } if ( local && !local.external && allowedActions.some(action => action === local.type) ) { bus.emit('global.event', local); } } };
Y el último: montar la saga con los eventos necesarios:
import { fork } from 'redux-saga/effects'; import { globalEventBusSaga } from './global-event-bus'; import { DropdownsActions } from '../actions/dropdowns'; import { AreasActions } from '../actions/areas'; export function * sagas() { yield fork(globalEventBusSaga, [ DropdownsActions.closeAll, AreasActions.add, AreasActions.remove, ... ]); }
Espero poder demostrar que las sagas facilitan la descripción de los efectos secundarios complejos. Explore las API de la biblioteca, transfiera a sus casos, componga patrones complejos de expectativas de eventos y sea feliz. ¡Nos vemos en los espacios abiertos de JS!