Sagrado da vida

Bom dia


Você também tem um desenvolvedor de reação familiar que conta histórias incríveis sobre efeitos colaterais no redux? Não ?! Posso me tornar essa pessoa



O autor teve a liberdade de não escrever uma parte introdutória sobre o que é a biblioteca redux saga. Ele espera que, em caso de dados insuficientes, um leitor generoso use a pesquisa Habr ou o tutorial oficial . Esses exemplos são bastante simplificados para transmitir a essência.


Então, por que eu coloquei todos vocês juntos? Será sobre o uso da redux saga nos espaços abertos dos clientes de combate. Mais especificamente, sobre casos mais complexos e interessantes do que "aceitar ação => enviar solicitação de API => criar uma nova ação".


Espero estimular um estudo mais aprofundado desta biblioteca por parte de concidadãos, bem como compartilhar o prazer de como as coisas assíncronas sofisticadas se tornam mais compreensíveis e expressivas.


Websockets


Caso de uso: receba atualizações da lista de vagas disponíveis do servidor em tempo real usando o modelo push.


É, obviamente, sobre o uso de soquetes da web. Por exemplo, considere socket.io, mas, de fato, a API do soquete não importa aqui.


Nas sagas, existe um canal. Este é um barramento de mensagens através do qual a fonte de eventos pode se comunicar com seu consumidor. O principal objetivo dos canais é a comunicação entre sagas e a conversão do fluxo de eventos assíncronos em uma estrutura conveniente para o trabalho.


Por padrão, store é o principal canal de eventos da redux saga. Eventos vêm na forma de ação. Canais são usados ​​para trabalhar com eventos que não são da loja.


Acontece que o canal é exatamente o que você precisa para trabalhar com o fluxo assíncrono de mensagens do soquete. Vamos criar o canal o mais rápido possível!


Mas primeiro, crie um soquete:


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

Agora declare uma lista modesta de eventos:


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

A seguir, é um método de fábrica para criar um canal. O código cria um método para assinar eventos de interesse para nós do soquete, um método para cancelar a inscrição e, diretamente, o próprio canal de eventos:


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

Vamos escrever uma saga bastante simples, aguardando atualizações do soquete e convertendo-as na ação correspondente:


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

Resta apenas ligá-lo à saga raiz:


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

Preenchimento automático do Google Places


Caso de uso: mostre dicas quando o usuário entrar em um local geográfico para pesquisa subsequente de imóveis próximos.


De fato, precisamos das coordenadas e o usuário precisa do nome legível por humanos da área desejada.


Parece que essa tarefa difere da chata "ação => API => ação"? No caso de preenchimento automático, queremos fazer o mínimo possível de chamadas inúteis para recursos externos e também mostrar ao usuário apenas dicas relevantes.


Primeiro, escreveremos um método de API utilizando o Serviço de preenchimento automático do Google Places. Uma das coisas interessantes aqui é a limitação de avisos em um determinado país:


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

Existe um método de API que vamos usar, você pode começar a escrever uma saga. É hora de esclarecer pedidos inúteis.


A implementação na testa, quando o usuário digita e, para cada alteração, lê - para cada personagem, envia uma solicitação à API - não leva a lugar algum. Enquanto o usuário está digitando, ele não precisa de dicas. Mas quando ele para, é hora de servi-lo.


Além disso, não gostaríamos de estar em uma situação em que o usuário digitasse alguma coisa, parasse, a solicitação da API desaparecesse, o usuário digitasse alguma coisa, outra solicitação desaparecesse.


Assim, criamos uma corrida entre duas solicitações. Os eventos podem se desenvolver de duas maneiras e ambas não são muito agradáveis.


Por exemplo, uma solicitação irrelevante será encerrada antes da atual e, por um momento, o usuário verá promessas irrelevantes. Desagradável, mas não crítico.


Ou, a solicitação atual termina mais cedo que a irrelevante e, depois de piscar, o usuário permanece com as mesmas solicitações irrelevantes. Isso já é crítico.


Obviamente, não somos os primeiros a encontrar esse problema e a técnica conhecida como debounce nos ajudará - a execução da tarefa somente após N unidades de tempo desde que o último evento foi recebido. Aqui está um pouco de material sobre isso.


Na saga redux, essa técnica é implementada usando dois efeitos - delay e takeLatest . O primeiro adia a execução da saga pelo número especificado de milissegundos. O segundo interrompe a execução de uma saga já em funcionamento quando um novo evento chega.


Sabendo tudo isso, escreveremos uma 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); }; 

Como no exemplo anterior, resta apenas ligá-lo à saga raiz:


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


Caso de uso: feche as listas suspensas auto-escritas ao clicar fora do território do controle.


De fato, essa é uma emulação do comportamento incorporado na seleção do navegador. As razões pelas quais você pode precisar de uma lista suspensa escrita em divs serão deixadas para a imaginação do leitor.


Um recurso importante da tarefa que está sendo resolvida é a passagem de um evento fora do controle, por exemplo, ao clicar fora da lista.


Adivinhou? Sim, os canais também nos ajudarão aqui. Utilizando-os, transformaremos os eventos de clique que aparecem na parte superior na ação correspondente.


Seria bom ter um método de fábrica que crie canais para um evento de janela arbitrário. E aqui está ele:


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

Criamos uma saga muito semelhante ao primeiro exemplo (você pode criar um método de fábrica para eles, se desejar):


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

Os redutores interessados ​​colocarão o controle em um estado fechado:


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

A própria lista suspensa deve interromper a propagação do evento click em qualquer parte interna e enviar o evento de fechamento para a loja por conta própria. Por exemplo, quando você clica para abrir:


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

Caso contrário, a lista simplesmente não será aberta. O mesmo se aplica a um clique ao selecionar uma opção.


El clássico, monte a saga:


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

Notificações


Caso de uso: mostre as notificações do navegador sobre a disponibilidade de novas vagas, se a guia estiver em segundo plano.


Na guia ativa, o usuário verá a alteração em um controle especial e, portanto, as notificações não são apropriadas. Mas para a guia segundo plano pode ser útil. Claro, com a permissão do usuário!


Também gostaria de clicar na notificação para ir para a guia e mostrar novas vagas. Se o usuário não responder, feche a notificação. Para fazer isso, precisamos de outro efeito útil - raça . Ele permite que você organize uma corrida entre vários outros efeitos. Na maioria dos casos, a corrida é usada para fornecer um tempo limite para algumas operações.


Omitimos o código para rastrear a atividade da guia, devido à identidade com o código de interceptação de cliques do exemplo anterior.


Escreveremos um método de fábrica que criará um canal para solicitar a aprovação de um usuário para receber notificações:


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

Em busca de outro método de fábrica, mas com um canal para receber uma notificação, clique em:


 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 os canais são de uso único e gravação máxima em um evento, após o qual são fechados.


A chave permanece - a saga com lógica. Verifique se a guia está ativa, solicite permissão, crie uma notificação, aguarde um clique ou tempo limite, mostre novas vagas, torne a guia ativa e feche a notificação:


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

Monte a saga antes de fazer esta detecção de recurso:


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

Barramento de evento global


Caso de uso: transfira a categoria de eventos especificada entre redux-stores.


Esse barramento é necessário se várias aplicações com dados comuns estiverem presentes na página. Ao mesmo tempo, os aplicativos podem ser implementados independentemente um do outro.


Por exemplo, uma sequência de pesquisa com filtros e resultados da pesquisa como aplicativos de reação separados. Ao trocar de filtro, quero que o aplicativo de resultados saiba sobre ele, se ele também estiver na página.


Usamos o emissor de evento padrão:


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

O já amado eventChannel transforma um emissor padrão em um 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); } 

A saga é bastante simples - criamos um canal e aceitamos incessantemente eventos, internos ou externos. Se recebemos um evento interno, envie-o para o barramento, se o externo - na loja:


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

E o final - montando a saga com os eventos necessários:


 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 mostrar que as sagas facilitam a descrição de efeitos colaterais complexos. Explore APIs de biblioteca, transfira para seus casos, componha padrões complexos de expectativa de eventos e seja feliz. Vemo-nos nos espaços abertos da JS!

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


All Articles