Водич за почетнике за РкЈС & Редук Обсервабле

Редук-Обсервабле је међупрограм заснован на РкЈС за Редук који програмерима омогућава рад са асинхроним акцијама. То је алтернатива редук-тхунк и редук-сага.

Овај чланак покрива основе РкЈС, како да подесите Редук-Обсерваблес и неке од његових практичних примера. Али пре тога, морамо да разумемо образац посматрача .

Узорак посматрача

У обрасцу Обсервер, објекат под називом „Обсервабле“ или „Субјецт“, одржава колекцију претплатника под називом „Обсерверс“. Када се стање субјеката промени, обавештава све своје Посматраче.

У ЈаваСцрипт-у најједноставнији пример би били емитери догађаја и обрађивачи догађаја.

Када то учините .addEventListener, гурате посматрача у колекцију посматрача субјекта. Кад год се догађај догоди, субјекат обавести све посматраче.

РкЈС

Према службеној веб страници,

РкЈС је ЈаваСцрипт имплементација РеацтивеКс-а, библиотеке за компоновање асинхроних програма и програма заснованих на догађајима помоћу уочљивих секвенци.

Једноставно речено, РкЈС је примена Обсервер обрасца. Такође проширује образац Обсервер пружајући операторе који нам омогућавају да декларативно компонујемо Обсервабле и Субјецтс.

Посматрачи, Посматрачи, Оператери и Субјекти су градивни блокови РкЈС. Дакле, погледајмо сада сваку детаљније.

Посматрачи

Посматрачи су објекти који се могу претплатити на Обсерваблес анд Субјецтс. Након претплате могу да добијају обавештења три врсте - следећа, грешка и комплетна.

Било који објекат са следећом структуром може се користити као посматрач.

interface Observer { closed?: boolean; next: (value: T) => void; error: (err: any) => void; complete: () => void; }

Када видљиве гура даље, грешке, и комплетне обавештења, посматрача .next, .errorи .completeсе позивају методе.

Посматрано

Посматрани су објекти који могу емитовати податке током одређеног временског периода. Може се представити помоћу „мермерног дијаграма“.

Тамо где хоризонтална линија представља време, кружни чворови представљају податке које емитује Обсервабле, а вертикална линија показује да је Обсервабле успешно завршен.

Посматрачи могу наићи на грешку. Крст представља грешку коју емитује Обсервабле.

Стања „довршено“ и „грешка“ су коначна. То значи да Обсерваблес не могу емитовати никакве податке након што успешно заврше или наиђу на грешку.

Стварање видљивог

Обсерваблес се креирају помоћу new Observableконструктора који узима један аргумент - функцију претплате. Такође се могу створити уочљиви помоћу неких оператора, али о томе ћемо касније када говоримо о операторима.

import { Observable } from 'rxjs'; const observable = new Observable(subscriber => { // Subscribe function });

Претплата на Обсервабле

Посматрани се могу претплатити коришћењем њихове .subscribeметоде и прослеђивањем посматрача.

observable.subscribe({ next: (x) => console.log(x), error: (x) => console.log(x), complete: () => console.log('completed'); });

Извршење посматраног

Функција претплате коју смо проследили new Observableконструктору извршава се сваки пут када се Обсервабле претплати.

Функција претплата узима један аргумент - претплатник. Претплатник подсећа на структуру посматрачи, а има исте 3 методе: .next, .errorи .complete.

Посматрани могу методом да шаљу податке посматрачу .next. Ако је Обсервабле успешно завршен, може обавестити Обсервер користећи .completeметоду. Ако је Обсервабле наишао на грешку, помоћу .errorметоде може гурнути грешку на Обсервер .

// Create an Observable const observable = new Observable(subscriber => { subscriber.next('first data'); subscriber.next('second data'); setTimeout(() => { subscriber.next('after 1 second - last data'); subscriber.complete(); subscriber.next('data after completion'); //  console.log(x), error: (x) => console.log(x), complete: () => console.log('completed') }); // Outputs: // // first data // second data // third data // after 1 second - last data // completed

Посматрано је једнозначно

Опсервабле су Уницаст , што значи опсервабле може имати највише један претплатника. Када се Обсервер претплати на Обсервабле, добија копију Обсервабле-а која има сопствену путању извршења, чинећи Обсерваблес уницаст-ом.

То је као да гледате ИоуТубе видео. Сви гледаоци гледају исти видео садржај, али могу да гледају различите сегменте видео записа.

Пример : направимо Обсервабле који емитује 1 до 10 током 10 секунди. Затим се претплатите на Обсервабле једном одмах и поново након 5 секунди.

// Create an Observable that emits data every second for 10 seconds const observable = new Observable(subscriber => { let count = 1; const interval = setInterval(() => { subscriber.next(count++); if (count > 10) { clearInterval(interval); } }, 1000); }); // Subscribe to the Observable observable.subscribe({ next: value => { console.log(`Observer 1: ${value}`); } }); // After 5 seconds subscribe again setTimeout(() => { observable.subscribe({ next: value => { console.log(`Observer 2: ${value}`); } }); }, 5000); /* Output Observer 1: 1 Observer 1: 2 Observer 1: 3 Observer 1: 4 Observer 1: 5 Observer 2: 1 Observer 1: 6 Observer 2: 2 Observer 1: 7 Observer 2: 3 Observer 1: 8 Observer 2: 4 Observer 1: 9 Observer 2: 5 Observer 1: 10 Observer 2: 6 Observer 2: 7 Observer 2: 8 Observer 2: 9 Observer 2: 10 */

У излазу можете приметити да је други посматрач почео да штампа од 1, иако се претплатио након 5 секунди. То се дешава јер је други посматрач добио копију Обсервабле-а чија је претплатна функција поново позвана. Ово илуструје једнозначно понашање Обсерваблес-а.

Предмети

Предмет је посебна врста посматрача.

Креирање предмета

Предмет се креира помоћу new Subjectконструктора.

import { Subject } from 'rxjs'; // Create a subject const subject = new Subject();

Претплата на тему

Претплата на предмет је слична претплати на Обсервабле: ви користите .subscribeметод и прослеђујете Обсервер-а.

subject.subscribe({ next: (x) => console.log(x), error: (x) => console.log(x), complete: () => console.log("done") });

Извршење предмета

За разлику од уочљивости, предмет зове своје .next, .errorи .completeметоде гурнути податке посматрача.

// Push data to all observers subject.next('first data'); // Push error to all observers subject.error('oops something went wrong'); // Complete subject.complete('done');

Предмети су мултицаст

Предмети су мултицаст: више посматрача дели исти Предмет и његову путању извршења. То значи да се сва обавештења емитују свим посматрачима. То је као да гледате програм уживо. Сви гледаоци истовремено гледају исти сегмент истог садржаја.

Пример: направимо Субјект који емитује 1 до 10 током 10 секунди. Затим се претплатите на Обсервабле једном одмах и поново након 5 секунди.

// Create a subject const subject = new Subject(); let count = 1; const interval = setInterval(() => { subscriber.next(count++); if (count > 10) { clearInterval(interval); } }, 1000); // Subscribe to the subjects subject.subscribe(data => { console.log(`Observer 1: ${data}`); }); // After 5 seconds subscribe again setTimeout(() => { subject.subscribe(data => { console.log(`Observer 2: ${data}`); }); }, 5000); /* OUTPUT Observer 1: 1 Observer 1: 2 Observer 1: 3 Observer 1: 4 Observer 1: 5 Observer 2: 5 Observer 1: 6 Observer 2: 6 Observer 1: 7 Observer 2: 7 Observer 1: 8 Observer 2: 8 Observer 1: 9 Observer 2: 9 Observer 1: 10 Observer 2: 10 */ 

In the output, you can notice that the second Observer started printing from 5 instead of starting from 1. This happens because the second Observer is sharing the same Subject. Since it subscribed after 5 seconds, the Subject has already finished emitting 1 to 4. This illustrates the multicast behavior of a Subject.

Subjects are both Observable and Observer

Subjects have the .next, .error and .complete methods. That means that they follow the structure of Observers. Hence, a Subject can also be used as an Observer and passed to the .subscribe function of Observables or other Subjects.

Example: let us create an Observable and a Subject. Then subscribe to the Observable using the Subject as an Observer. Finally, subscribe to the Subject. All the values emitted by the Observable will be pushed to the Subject, and the Subject will broadcast the received values to all its Observers.

// Create an Observable that emits data every second const observable = new Observable(subscriber => { let count = 1; const interval = setInterval(() => { subscriber.next(count++); if (count > 5) { clearInterval(interval); } }, 1000); }); // Create a subject const subject = new Subject(); // Use the Subject as Observer and subscribe to the Observable observable.subscribe(subject); // Subscribe to the subject subject.subscribe({ next: value => console.log(value) }); /* Output 1 2 3 4 5 */

Operators

Operators are what make RxJS useful. Operators are pure functions that return a new Observable. They can be categorized into 2 main categories:

  1. Creation Operators
  2. Pipeable Operators

Creation Operators

Creation Operators are functions that can create a new Observable.

Example: we can create an Observable that emits each element of an array using the from operator.

const observable = from([2, 30, 5, 22, 60, 1]); observable.subscribe({ next: (value) => console.log("Received", value), error: (err) => console.log(err), complete: () => console.log("done") }); /* OUTPUTS Received 2 Received 30 Received 5 Received 22 Received 60 Received 1 done */

The same can be an Observable using the marble diagram.

Pipeable Operators

Пипеабле Операторс су функције које узимају Обсервабле као улаз и враћају нови Обсервабле са измењеним понашањем.

Пример: узмимо Обсервабле који смо креирали помоћу fromоператора. Сада, користећи овај Обсервабле, можемо створити нови Обсервабле који емитује само бројеве веће од 10 помоћу filterоператора.

const greaterThanTen = observable.pipe(filter(x => x > 10)); greaterThanTen.subscribe(console.log, console.log, () => console.log("completed")); // OUTPUT // 11 // 12 // 13 // 14 // 15

Исто се може представити помоћу мермерног дијаграма.

Постоји много више корисних оператора. Комплетну листу оператора, заједно са примерима, можете видети овде у званичној РкЈС документацији.

Кључно је разумјети све често кориштене операторе. Ево неколико оператера које често користим:

  1. mergeMap
  2. switchMap
  3. exhaustMap
  4. map
  5. catchError
  6. startWith
  7. delay
  8. debounce
  9. throttle
  10. interval
  11. from
  12. of

Редук Обсерваблес

Према службеној веб страници,

Средњи софтвер заснован на РкЈС за Редук. Саставите и откажите асинхријске акције да бисте створили нежељене ефекте и још много тога.

У Редук-у, кад год се пошаље радња, она пролази кроз све функције редуктора и враћа се ново стање.

Редук-опсервабле предузима све ове послане акције и нова стања и од тога ствара две видљиве - акције које се могу опазити action$и државе које се могу посматрати state$.

Радње које се могу емитовати све радње које се шаљу помоћу store.dispatch(). Државе које се могу емитовати све нове државне објекте које је вратио коренски редуктор.

Епс

Према службеној веб страници,

То је функција која узима ток радњи и враћа ток радњи. Акције унутра, акције ван.

Epics are functions that can be used to subscribe to Actions and States Observables. Once subscribed, epics will receive the stream of actions and states as input, and it must return a stream of actions as an output. Actions In - Actions Out.

const someEpic = (action$, state$) => { return action$.pipe( // subscribe to actions observable map(action => { // Receive every action, Actions In return someOtherAction(); // return an action, Actions Out }) ) }

It is important to understand that all the actions received in the Epic have already finished running through the reducers.

Inside an Epic, we can use any RxJS observable patterns, and this is what makes redux-observables useful.

Example: we can use the .filter operator to create a new intermediate observable. Similarly, we can create any number of intermediate observables, but the final output of the final observable must be an action, otherwise an exception will be raised by redux-observable.

const sampleEpic = (action$, state$) => { return action$.pipe( filter(action => action.payload.age >= 18), // can create intermediate observables and streams map(value => above18(value)) // where above18 is an action creator ); }

Every action emitted by the Epics are immediately dispatched using the store.dispatch().

Setup

First, let's install the dependencies.

npm install --save rxjs redux-observable

Create a separate folder named epics to keep all the epics. Create a new file index.js inside the epics folder and combine all the epics using the combineEpics function to create the root epic. Then export the root epic.

import { combineEpics } from 'redux-observable'; import { epic1 } from './epic1'; import { epic2 } from './epic2'; const epic1 = (action$, state$) => { ... } const epic2 = (action$, state$) => { ... } export default combineEpics(epic1, epic2);

Create an epic middleware using the createEpicMiddleware function and pass it to the createStore Redux function.

import { createEpicMiddleware } from 'redux-observable'; import { createStore, applyMiddleware } from 'redux'; import rootEpic from './rootEpics'; const epicMiddleware = createEpicMiddlware(); const store = createStore( rootReducer, applyMiddleware(epicMiddlware) );

Finally, pass the root epic to epic middleware's .run method.

epicMiddleware.run(rootEpic);

Some Practical Usecases

RxJS has a big learning curve, and the redux-observable setup worsens the already painful Redux setup process. All that makes Redux observable look like an overkill. But here are some practical use cases that can change your mind.

Throughout this section, I will be comparing redux-observables with redux-thunk to show how redux-observables can be helpful in complex use-cases. I don't hate redux-thunk, I love it, and I use it every day!

1. Make API Calls

Usecase: Make an API call to fetch comments of a post. Show loaders when the API call is in progress and also handle API errors.

A redux-thunk implementation will look like this,

function getComments(postId){ return (dispatch) => { dispatch(getCommentsInProgress()); axios.get(`/v1/api/posts/${postId}/comments`).then(response => { dispatch(getCommentsSuccess(response.data.comments)); }).catch(() => { dispatch(getCommentsFailed()); }); } }

and this is absolutely correct. But the action creator is bloated.

We can write an Epic to implement the same using redux-observables.

const getCommentsEpic = (action$, state$) => action$.pipe( ofType('GET_COMMENTS'), mergeMap((action) => from(axios.get(`/v1/api/posts/${action.payload.postId}/comments`).pipe( map(response => getCommentsSuccess(response.data.comments)), catchError(() => getCommentsFailed()), startWith(getCommentsInProgress()) ) );

Now it allows us to have a clean and simple action creator like this,

function getComments(postId) { return { type: 'GET_COMMENTS', payload: { postId } } }

2. Request Debouncing

Usecase: Provide autocompletion for a text field by calling an API whenever the value of the text field changes. API call should be made 1 second after the user has stopped typing.

A redux-thunk implementation will look like this,

let timeout; function valueChanged(value) { return dispatch => { dispatch(loadSuggestionsInProgress()); dispatch({ type: 'VALUE_CHANGED', payload: { value } }); // If changed again within 1 second, cancel the timeout timeout && clearTimeout(timeout); // Make API Call after 1 second timeout = setTimeout(() => { axios.get(`/suggestions?q=${value}`) .then(response => dispatch(loadSuggestionsSuccess(response.data.suggestions))) .catch(() => dispatch(loadSuggestionsFailed())) }, 1000, value); } }

It requires a global variable timeout. When we start using global variables, our action creators are not longer pure functions. It also becomes difficult to unit test the action creators that use a global variable.

We can implement the same with redux-observable using the .debounce operator.

const loadSuggestionsEpic = (action$, state$) => action$.pipe( ofType('VALUE_CHANGED'), debounce(1000), mergeMap(action => from(axios.get(`/suggestions?q=${action.payload.value}`)).pipe( map(response => loadSuggestionsSuccess(response.data.suggestions)), catchError(() => loadSuggestionsFailed()) )), startWith(loadSuggestionsInProgress()) );

Now, our action creators can be cleaned up, and more importantly, they can be pure functions again.

function valueChanged(value) { return { type: 'VALUE_CHANGED', payload: { value } } }

3. Request Cancellation

Usecase: Continuing the previous use-case, assume that the user didn't type anything for 1 second, and we made our 1st API call to fetch the suggestions.

Let's say the API itself takes an average of 2-3 seconds to return the result. Now, if the user types something while the 1st API call is in progress, after 1 second, we will make our 2nd API. We can end up having two API calls at the same time, and it can create a race condition.

To avoid this, we need to cancel the 1st API call before making the 2nd API call.

A redux-thunk implementation will look like this,

let timeout; var cancelToken = axios.cancelToken; let apiCall; function valueChanged(value) { return dispatch => { dispatch(loadSuggestionsInProgress()); dispatch({ type: 'VALUE_CHANGED', payload: { value } }); // If changed again within 1 second, cancel the timeout timeout && clearTimeout(timeout); // Make API Call after 1 second timeout = setTimeout(() => { // Cancel the existing API apiCall && apiCall.cancel('Operation cancelled'); // Generate a new token apiCall = cancelToken.source(); axios.get(`/suggestions?q=${value}`, { cancelToken: apiCall.token }) .then(response => dispatch(loadSuggestionsSuccess(response.data.suggestions))) .catch(() => dispatch(loadSuggestionsFailed())) }, 1000, value); } }

Now, it requires another global variable to store the Axios's cancel token. More global variables = more impure functions!

To implement the same using redux-observable, all we need to do is replace .mergeMap with .switchMap.

const loadSuggestionsEpic = (action$, state$) => action$.pipe( ofType('VALUE_CHANGED'), throttle(1000), switchMap(action => from(axios.get(`/suggestions?q=${action.payload.value}`)).pipe( map(response => loadSuggestionsSuccess(response.data.suggestions)), catchError(() => loadSuggestionsFailed()) )), startWith(loadSuggestionsInProgress()) );

Since it doesn't require any changes to our action creators, they can continue to be pure functions.

Слично томе, постоји много случајева примене у којима Редук-Обсерваблес заправо блиста! На пример, анкетирање АПИ-ја, приказивање залогајница, управљање ВебСоцкет везама итд.

Закључити

Ако развијате Редук апликацију која укључује тако сложене случајеве употребе, топло се препоручује да користите Редук-Обсерваблес. Напокон, користи од његове употребе су пропорционалне сложености ваше апликације, а то је видљиво из горе поменутих практичних случајева примене.

Чврсто верујем да ће нам коришћење исправног скупа библиотека помоћи да развијемо много чистије и одрживе апликације, а дугорочно ће користи од њихове употребе надмашити недостатке.