Sag de la vie

Bonjour.


Avez-vous également un développeur familier React qui raconte des histoires étonnantes sur les effets secondaires dans Redux? Non?! Puis-je devenir cette personne



L'auteur a pris la liberté de ne pas écrire une partie introductive sur ce qu'est la bibliothèque de la saga redux. Il espère qu'en cas de données insuffisantes, un lecteur généreux utilisera la recherche Habr ou le tutoriel officiel . Ces exemples sont grandement simplifiés pour transmettre l'essence.


Alors, pourquoi je vous ai tous réunis. Il s'agira de l'utilisation de la saga redux dans les espaces ouverts des clients de combat. Plus précisément, sur les cas plus complexes et intéressants que "accepter une action => envoyer une requête API => créer une nouvelle action".


J'espère stimuler une étude plus approfondie de cette bibliothèque par des concitoyens, ainsi que partager le plaisir de voir comment les choses asynchrones sophistiquées deviennent plus compréhensibles et expressives.


Websockets


Cas d'utilisation: recevez des mises à jour de la liste des offres d'emploi disponibles du serveur en temps réel à l'aide du modèle push.


Il s'agit bien sûr de l'utilisation des sockets web. Par exemple, prenez socket.io, mais en fait l'API socket n'a pas d'importance ici.


Dans les sagas, il existe une chaîne. Il s'agit d'un bus de messages à travers lequel la source des événements peut communiquer avec son consommateur. L'objectif principal des canaux est la communication entre les sagas et la conversion du flux d'événements asynchrones en une structure pratique pour le travail.


Par défaut, store est le principal canal d'événement de la saga redux. Les événements se présentent sous forme d'action. Les canaux sont utilisés pour travailler avec des événements ne provenant pas du magasin.


Il s'avère que le canal est exactement ce dont vous avez besoin pour travailler avec le flux asynchrone de messages de la socket. Créons la chaîne dès que possible!


Mais d'abord, créez un socket:


import io from 'socket.io-client'; export const socket = io.connect('/'); 

Déclarez maintenant une liste modeste d'événements:


 export const SocketEvents = { jobsFresh: 'jobs+fresh', }; 

Vient ensuite une méthode d'usine pour créer un canal. Le code crée une méthode pour s'abonner aux événements qui nous intéressent à partir de la socket, une méthode pour se désinscrire et, directement, le canal d'événements lui-même:


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

Écrivons une saga assez simple, en attendant les mises à jour du socket et en les convertissant en l'action correspondante:


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

Il ne reste plus qu'à le lier à la saga racine:


 import { fork } from 'redux-saga/effects'; import { freshJobsSaga } from './fresh-jobs'; export function * sagas() { yield fork(freshJobsSaga); } 

Saisie semi-automatique de Google Adresses


Cas d'utilisation: affichez des conseils lorsque l'utilisateur entre dans une zone géographique pour une recherche ultérieure de biens immobiliers à proximité.


En fait, nous avons besoin des coordonnées et l'utilisateur a besoin du nom lisible par l'homme de la zone souhaitée.


Il semblerait que cette tâche diffère de l'ennuyeuse "action => API => action"? Dans le cas de la saisie semi-automatique, nous souhaitons effectuer le moins d'appels inutiles aux ressources externes possible, et n'afficher que les conseils pertinents pour l'utilisateur.


Tout d'abord, nous allons écrire une méthode API utilisant le service de saisie semi-automatique de Google Adresses. Ce qui est intéressant ici, c'est la limitation des invites dans un pays donné:


 export function getPlaceSuggestions(autocompleteService, countryCode, query) { return new Promise(resolve => { autocompleteService.getPlacePredictions({ componentRestrictions: { country: countryCode }, input: query, }, resolve); }); } 

Il y a une méthode API que nous allons tirer, vous pouvez commencer à écrire une saga. Il est temps de clarifier les demandes inutiles.


L'implémentation dans le front, lorsque l'utilisateur tape, et nous pour chaque changement, lisons - pour chaque caractère, envoyons une requête à l'API - ne mène nulle part. Pendant que l'utilisateur tape, il n'a pas besoin d'indices. Mais quand il s'arrête, il est temps de le servir.


De plus, nous ne voudrions pas être dans une situation où l'utilisateur a tapé quelque chose, s'est arrêté, la demande d'API a disparu, l'utilisateur a tapé quelque chose, une autre demande a disparu.


Ainsi, nous avons créé une course entre deux demandes. Les événements peuvent se développer de deux manières et les deux ne sont pas très agréables.


Par exemple, une demande non pertinente se terminera plus tôt que la demande actuelle et pendant un moment, l'utilisateur verra des invites non pertinentes. Désagréable, mais pas critique.


Ou bien, la demande en cours se termine plus tôt que la requête non pertinente et après avoir clignoté, l'utilisateur restera avec les mêmes invites non pertinentes. C'est déjà critique.


Bien sûr, nous ne sommes pas les premiers à rencontrer un tel problème et la technique connue sous le nom de debounce nous aidera - exécution des tâches uniquement après N unités de temps depuis la réception du dernier événement. Voici un peu de matériel à ce sujet.


Dans la saga redux, cette technique est implémentée en utilisant deux effets - delay et takeLatest . La première retarde l'exécution de la saga du nombre de millisecondes spécifié. Le second interrompt l'exécution d'une saga déjà en marche lorsqu'un nouvel événement arrive.


Sachant tout cela, nous écrirons une 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(); //  API    , //      if (!isApiLoaded) { yield take(MapsActions.apiLoaded); } //     Google Places Autocomplete  store const { maps: { autocompleteService }, countryCode } = yield select(); //    , //        if (query) { yield put(PlaceActions.suggestions([])); yield put(PlaceActions.select(null)); return; } //  250    yield call(delay, 250); //  API  const suggestions = yield call( getPlaceSuggestions, autocompleteService, countryCode, query, ); //  action   const action = PlacesActions.suggestions(suggestions || []); //     store yield put(action); }; 

Comme dans l'exemple précédent, il ne reste plus qu'à le lier à la saga racine:


 import { takeLatest } from 'redux-saga/effects'; import { PlaceActions } from '../actions/place'; import { placeSuggestionsSaga } from './place-suggestions'; export function * sagas() { yield takeLatest( PlaceActions.changeQuery, placeSuggestionsSaga, ); } 


Cas d'utilisation: fermez les listes déroulantes auto-écrites lorsque vous cliquez en dehors du territoire du contrôle.


En fait, il s'agit d'une émulation du comportement intégré à la sélection du navigateur. Les raisons pour lesquelles vous pourriez avoir besoin d'une liste déroulante écrite sur des divs seront laissées à l'imagination du lecteur.


Une caractéristique clé de la tâche en cours de résolution est le passage d'un événement en dehors du contrôle, par exemple, lorsque vous cliquez en dehors de la liste.


Deviné? Oui, les chaînes nous aideront également ici. En les utilisant, nous transformerons les événements de clic qui apparaissent tout en haut en l'action correspondante.


Ce serait bien d'avoir une méthode d'usine qui crée des canaux pour un événement de fenêtre arbitraire. Et le voici:


 import { eventChannel } from 'redux-saga'; export function createWindowEventChannel(eventName) { const subscribe = emitter => { window.addEventListener(eventName, emitter); return () => window.removeEventListener(eventName, emitter); }; return eventChannel(subscribe); } 

Nous créons une saga très similaire au premier exemple (vous pouvez créer une méthode d'usine pour eux si vous le souhaitez):


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

Les réducteurs intéressés mettront le contrôle dans un état fermé:


 import { handleActions } from 'redux-actions'; import { DropdownActions } from '../actions/dropdown'; export const priceReducer = handleActions({ ..., [DropdownActions.closeAll]: state => ({ ...state, isOpen: false}), }, {}); 

La liste déroulante elle-même doit arrêter la propagation de l'événement de clic sur toutes les parties internes et envoyer l'événement de fermeture au magasin par lui-même. Par exemple, lorsque vous cliquez pour ouvrir:


 // components/dropdown.js import React from 'react'; export class Dropdown extends React.Component { ... __open(event) { event.stopPropagation(); this.props.open(); } } // dispatchers/open-price-dropdown.js import { DropdownActions } from '../actions/dropdown'; import { PriceActions } from '../actions/price'; export const openPriceDropdownDispatcher = dispatch => () => { dispatch( DropdownActions.closeAll() ); dispatch( PriceActions.open() ); }; 

Sinon, la liste ne s'ouvrira tout simplement pas. La même chose s'applique à un clic lors de la sélection d'une option.


El clasico, montez la saga:


 import { fork } from 'redux-saga/effects'; import { closeDropdownsSaga } from './close-dropdowns'; export function * sagas() { yield fork(closeDropdownsSaga); } 

Notifications


Cas d'utilisation: affichez les notifications du navigateur sur la disponibilité de nouveaux postes vacants, si l'onglet est en arrière-plan.


Dans l'onglet actif, l'utilisateur voit un changement dans un contrôle spécial et les notifications ne sont donc pas appropriées. Mais l'onglet arrière-plan peut être utile. Bien sûr, avec la permission de l'utilisateur!


Je voudrais également cliquer sur la notification pour accéder à l'onglet et afficher les nouveaux postes vacants. Si l'utilisateur ne répond pas, fermez la notification. Pour ce faire, nous avons besoin d'un autre effet utile - la race . Il vous permet d'organiser une course entre plusieurs autres effets. Dans la plupart des cas, la course est utilisée pour fournir un délai d'attente pour certaines opérations.


Nous omettons le code de suivi de l'activité de l'onglet, en raison de l'identité avec le code d'interception des clics de l'exemple précédent.


Nous allons écrire une méthode d'usine qui créera un canal pour demander l'approbation d'un utilisateur pour recevoir des notifications:


 import { eventChannel, END } from 'redux-saga'; export function createRequestNotificationPermissionChannel() { const subscribe = emitter => { Notification.requestPermission(permission => { emitter(permission); emitter(END); }); return () => {}; }; return eventChannel(subscribe); } 

À la poursuite d'une autre méthode d'usine, mais avec un canal pour recevoir une notification, cliquez sur:


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

Les deux canaux sont à usage unique et tirent au maximum sur un événement, après quoi ils sont fermés.


La clé reste - la saga avec la logique. Vérifiez si l'onglet est actif, demandez une autorisation, créez une notification, attendez un clic ou un délai d'expiration, affichez les nouveaux postes vacants, activez l'onglet, puis fermez la notification:


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

Montez la saga avant de faire cette détection de fonctionnalités:


 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 d'événement mondial


Cas d'utilisation: transférez la catégorie d'événements spécifiée entre les magasins redux.


Un tel bus est nécessaire si plusieurs applications avec des données communes sont présentes sur la page. Dans le même temps, les applications peuvent être mises en œuvre indépendamment les unes des autres.


Par exemple, une chaîne de recherche avec des filtres et des résultats de recherche en tant qu'applications de réaction distinctes. Lors du changement de filtres, je veux que l'application de résultats le sache si elle est également sur la page.


Nous utilisons l'émetteur d'événements standard:


 import EventEmmiter from 'events'; if (!window.GlobalEventBus) { window.GlobalEventBus = new EventEmmiter(); } export const globalEventBus = window.GlobalEventBus; 

L' événement déjà aimé, Channel, transforme un émetteur standard en 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 est assez simple - nous créons une chaîne et acceptons sans fin les événements, internes ou externes. Si nous avons reçu un événement interne, envoyez-le au bus, si externe - dans le magasin:


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

Et la dernière - monter la saga avec les événements nécessaires:


 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, ... ]); } 



J'espère pouvoir montrer que les sagas facilitent la description des effets secondaires complexes. Explorez les API de bibliothèque, transférez-les dans vos cas, composez des modèles complexes d'attente d'événements et soyez heureux. Rendez-vous aux espaces ouverts JS!

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


All Articles