+ - 0:00:00
Notes for current slide
Notes for next slide

Making async reasonable with redux saga

Redux Saga Logo

1 / 32

Agenda

  • Why redux-saga?

  • What is redux-saga?

  • What is Yield?

  • Callbacks vs. Promises vs. Yield

  • Effects

  • Channels

2 / 32

Why redux-saga?

  • Makes your code more reasonable

  • Easy to test (no mocks)

  • No circular dependencies

  • Ideal for common real-world applications

  • Works on both client and server

  • Large and growing contributing user base redux-saga github stars

3 / 32

What is redux-saga?

  • Redux middleware Redux Logo

  • Manages side effects (API, DB, logs, etc.)

  • Listen for actions, dispatches other actions, (using effects)

  • Maintains continuously running process called sagas

  • Uses Yield keyword

4 / 32

What is Yield?

  • Special keyword that can delay the execution of subsequent code

  • Only works inside generator functions

  • Works with promises and condenses code surrounding them

5 / 32

Callbacks vs. Promises vs. Yield

6 / 32

Async example with callbacks

api.get(URL, function callback(data){
// code execution resumes here
});
// code outside callback runs before callback resolution
7 / 32

Async example with callbacks x3

Code tends to drift to the right with more nested callbacks. (Callback hell)

api.get(URL_A, function callback(dataA){
api.get(URL_B, function callback(dataB){
api.get(URL_C, function callback(dataC){
// ...
});
});
});
// code outside callbacks runs before callbacks resolutions
8 / 32

Async example with callbacks x6

Code tends to drift to the right with more nested callbacks. (Callback hell)

api.get(URL_A, function callback(dataA){
api.get(URL_B, function callback(dataB){
api.get(URL_C, function callback(dataC){
api.get(URL_D, function callback(dataD){
api.get(URL_E, function callback(dataE){
api.get(URL_F, function callback(dataF){
// ...
});
});
});
});
});
});
// code outside callbacks runs before callbacks resolutions
9 / 32

Async example with promises

api.get(URL)
.then(data => {
// code execution resumes here
});
// code after ".then()" runs before promise resolution
10 / 32

Async example with promises x3

Code tends to grow vertically with additional "then" calls

api.get(URL_A)
.then(dataA => {
return api.get(URL_B);
})
.then(dataB => {
return api.get(URL_C);
})
.then(dataC => {
// ...
});
// code after ".then()" runs before all promises resolutions
11 / 32

Async example with yield

const data = yield api.get(URL);
// Execution resumes here. No code can run before promise resolution.

Code meant to be executed after call resolves can be placed on next line, as with synchronous code

No additional scope required

12 / 32

Async example with yield x3

Code is always compact

const dataA = yield api.get(URL_A);
const dataB = yield api.get(URL_B);
const dataC = yield api.get(URL_C);
// Execution resumes here. No code can run before all promises resolutions.
13 / 32

Yield

Advantages Positive

  • Fewer lines of code

  • Less indentation (avoids "callback hell")

  • Easiest to read quickly, reason about

  • Easier to debug

  • Execution stops on unhandled error

Disadvantages Negative

  • Only works inside Generator Functions

  • Requires additional plugins

14 / 32

What is a Generator Function?

  • Special Javascript function denoted by *

  • Calling function returns a generator

  • Actual code is executed by calling "next" method

  • Can "yield" multiple values

function* generateId() {
var id = 0;
while(true) {
id = id + 1;
yield id;
}
}
const gen = generateId();
gen.next().value; // 1
gen.next().value; // 2
gen.next().value; // 3
15 / 32

Continuously running process example

Sagas can run forever

import { delay } from "redux-saga";
function* logEachSecond() {
while(true){
yield delay(1000);
console.log("Saga loop");
}
}
16 / 32

Effects

functions that return a plain JavaScript object and do not perform any execution.

  • The execution is performed by the middleware during the Iteration process.

  • The middleware examines each Effect description and performs the appropriate action.

17 / 32

select, call and put

select: gets a value from the store

call: calls any function, most used for side-effects

put: dispatches one action to the store

import { select, call, put } from "redux-saga/effects";
import actions from "../actions";
import api from "../api";
function* getWorksheet(worksheetId) {
const settings = yield select(state => state.settings[worksheetId]);
const data = yield call(api.getWorksheet, { worksheetId, settings });
yield put(actions.setWorksheet(data));
}
18 / 32

without select, call and put

How we do it today using observables:

export class GetWorksheetEffect implements IEffect {
static $inject = [StoreServiceName, ApiServiceName];
constructor(private storeService: StoreService, private api: ApiService) { }
handle(dispatcher: IDispatcher, action: Action): void {
if (action.type === GET_WORKSHEET) {
const { worksheetId } = action.payload;
this.storeService.selectFn(state => state.settings[worksheetId])
.take(1)
.switchMap(settings => {
return this.api.getWorksheet({ worksheetId, settings });
})
.subscribe((data) => {
dispatcher.dispatch(actions.setWorksheet(data));
});
}
}
}
19 / 32

redux-saga vs. observables

How we do it today using observables:

this.storeService.selectFn(state => state.settings[worksheetId])
.take(1)
.switchMap(settings => {
return this.api.getWorksheet({ worksheetId, settings });
})
.subscribe((data) => {
dispatcher.dispatch(actions.setWorksheet(data));
});

How you would do it with redux-saga:

const settings = yield select(state => state.settings[worksheetId]);
const data = yield call(api.getWorksheet, { worksheetId, settings });
yield put(actions.setWorksheet(data));
20 / 32

Effects are objects

That is why there is no mocks.

call(api.signIn, user);

Output:

{
'@@redux-saga/IO': true,
CALL: {
context: null,
fn: [Function: signIn],
args: [ [Object] ]
}
}
21 / 32

How to test?

function* signIn(user) {
const result = yield call(api.signIn, user);
if(result.ok) {
yield put(actions.setAuthUser(result));
} else {
yield put(actions.alertError(result));
}
}
beforeEach(() => {
gen = signIn(user);
expect(gen.next().value)
.toEqual(call(api.signIn, user));
});
test("signIn ok", () => {
expect(gen.next(resultOk).value)
.toEqual(put(actions.setAuthUser(resultOK)));
});
test("signIn error", () => {
expect(gen.next(resultError).value)
.toEqual(put(actions.alertError(resultError)));
});
22 / 32

Handle errors with Try Catch

import { select, call, put } from "redux-saga/effects";
import actions from "../actions";
import api from "../api";
function* getWorksheet(worksheetId) {
try{
const settings = yield select(state => state.settings[worksheetId]);
const data = yield call(api.getWorksheet, { worksheetId, settings });
yield put(actions.setWorksheet(data));
} catch (error) {
yield put(actions.logError(error));
}
}
23 / 32

takeEvery

starts a new saga for each dispatched action.

import { takeEvery } from "redux-saga/effects";
function* handleActionA(action) {
// ...
}
function* watchActions() {
yield takeEvery('ACTION_A', handleActionA);
}
24 / 32

takeLatest

cancels the current Saga if it is running and starts the latest dispatched action.

import { takeLatest } from "redux-saga/effects";
function* handleActionA(action) {
// ...
}
function* watchActions() {
yield takeLatest('ACTION_A', handleActionA);
}
25 / 32

race

Sometimes we start multiple tasks in parallel but we don't want to wait for all of them, we just need to get the winner.

import { race, take, put } from 'redux-saga/effects'
import { delay } from 'redux-saga'
function* fetchPostsWithTimeout() {
const [posts, timeout] = yield race([
call(fetchApi, '/posts'),
call(delay, 1000)
]);
if (posts)
put({type: 'POSTS_RECEIVED', posts});
else
put({type: 'TIMEOUT_ERROR'});
}
26 / 32

all

waits all calls to return.

import api from '../api';
import { all, call } from `redux-saga/effects`;
function* mySaga() {
const [customers, products] = yield all([
call(api.fetchCustomers),
call(api.fetchProducts)
]);
}
27 / 32

throttle

ensures that the Saga will take at most one action during each period of specified time.

function* handleInput(input) {
// ...
}
function* watchInput() {
yield throttle(500, 'INPUT_CHANGED', handleInput);
}
28 / 32

take

waits until it gets the desired action.

import { put, take, call } from "redux-saga/effects";
import actions from "../actions";
import api from "../api";
function* removeUser(user) {
yield put(actions.confirmRemoveUser(user));
yield take("CONFIRM_OK");
yield call(api.removeUser, user);
}
29 / 32

take in a loop

waits until it gets the desired action.

import { actionChannel, take, call } from "redux-saga/effects";
import api from "../api";
function* logErrors() {
while(true) {
const action = yield take("LOG_ERROR");
yield call(api.log, action);
// ...
}
}
30 / 32

actionChannel

creates a queue of actions, you don't lose any action and you can process one by one.

import { actionChannel, take, call } from "redux-saga/effects";
import api from "../api";
function* logErrors() {
const channel = yield actionChannel("LOG_ERROR");
while(true) {
const action = yield take(channel);
yield call(api.log, action);
// ...
}
}
31 / 32

Agenda

  • Why redux-saga?

  • What is redux-saga?

  • What is Yield?

  • Callbacks vs. Promises vs. Yield

  • Effects

  • Channels

2 / 32
Paused

Help

Keyboard shortcuts

, , Pg Up, k Go to previous slide
, , Pg Dn, Space, j Go to next slide
Home Go to first slide
End Go to last slide
Number + Return Go to specific slide
b / m / f Toggle blackout / mirrored / fullscreen mode
c Clone slideshow
p Toggle presenter mode
t Restart the presentation timer
?, h Toggle this help
Esc Back to slideshow