Why redux-saga?
What is redux-saga?
What is Yield?
Callbacks vs. Promises vs. Yield
Effects
Channels
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 middleware
Manages side effects (API, DB, logs, etc.)
Listen for actions, dispatches other actions, (using effects)
Maintains continuously running process called sagas
Uses Yield keyword
Special keyword that can delay the execution of subsequent code
Only works inside generator functions
Works with promises and condenses code surrounding them
api.get(URL, function callback(data){ // code execution resumes here }); // code outside callback runs before callback resolution
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
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
api.get(URL) .then(data => { // code execution resumes here }); // code after ".then()" runs before promise resolution
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
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
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.
Fewer lines of code
Less indentation (avoids "callback hell")
Easiest to read quickly, reason about
Easier to debug
Execution stops on unhandled error
Only works inside Generator Functions
Requires additional plugins
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; // 1gen.next().value; // 2gen.next().value; // 3
Sagas can run forever
import { delay } from "redux-saga";function* logEachSecond() { while(true){ yield delay(1000); console.log("Saga loop"); }}
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.
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));}
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)); }); } }}
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));
That is why there is no mocks.
call(api.signIn, user);
Output:
{ '@@redux-saga/IO': true, CALL: { context: null, fn: [Function: signIn], args: [ [Object] ] }}
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)));});
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)); }}
starts a new saga for each dispatched action.
import { takeEvery } from "redux-saga/effects";function* handleActionA(action) { // ...}function* watchActions() { yield takeEvery('ACTION_A', handleActionA);}
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);}
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'});}
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) ]);}
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);}
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);}
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); // ... }}
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); // ... }}
Redux Saga https://app.pluralsight.com/library/courses/redux-saga
Advanced Redux https://app.pluralsight.com/library/courses/advanced-redux
Async React with Redux Saga https://egghead.io/courses/async-react-with-redux-saga
Redux Saga docs https://redux-saga.js.org/
Why redux-saga?
What is redux-saga?
What is Yield?
Callbacks vs. Promises vs. Yield
Effects
Channels
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 |