Redux-Saga V.S. Redux-Observable === ### Redux-Saga V.S. Redux-Observable 1. Metal Model 2. Side by side Comparison 3. Which one do I prefer and Why? 4. Futher Reading and Discussion --- ### Metal Model > Saga = **Worker** + **Watcher** > Rxjs = **Epic( Type + Operators )** ---- > Saga = **Worker** + **Watcher** ```javascript import API from '...' function* Watcher(){ yield takeEvery('do_thing', Worker) } function* Worker() { const users = yield API.get('/api/users') yield put({type:'done', users}) } ``` ---- > Rxjs = **Epic( Type + Operators )** ```javascript import API from '...' const Epic = action$ => action$ .ofType('do_thing') .flatMap(()=>API.get('/api/users')) .map(users=>({type:'done', users})) ``` --- ### Side by side Comparison ---- > Demo Part (1/3) 1. Fetch User 2. Fetch User (cancelable) 3. Do three things in sequence 4. Login, Logout, Cancel (with redux) ---- > Fetch User from **++/api/users/1++** > Saga ```javascript import axios from 'axios' function* watchSaga(){ yield takeEvery('fetch_user', fetchUser) // waiting for action (fetch_user) } function* fetchUser(action){ try { yield put({type:'fetch_user_ing'}) const response = yield call(axios.get,'/api/users/1') yield put({type:'fetch_user_done',user:response.data}) } catch (error) { yield put({type:'fetch_user_error',error}) } } ``` ---- > Fetch User from **++/api/users/1++** > Rxjs ```javascript import axios from 'axios' const fetchUserEpic = action$ => action$ .ofType('fetch_user') .map(()=>({type:'fetch_user_ing'})) .flatMap(()=>axios.get('/api/users/1')) .map(response=>response.data) .map(user=>({type:'fetch_user_done', user})) ``` ---- > Fetch User from **++/api/users/1++** (cancelable) > Saga ```javascript import { take, put, call, fork, cancel } from 'redux-saga/effects' import API from '...' function* fetchUser() { yield put({type:'fetch_user_ing'}) const user = yield call(API) yield put({type:'fetch_user_done', user}) } function* Watcher() { while(yield take('fetch_user')){ const bgSyncTask = yield fork(fetchUser) yield take('fetch_user_cancel') yield cancel(bgSyncTask) } } ``` ---- > Fetch User from **++/api/users/1++** (cancelable) > Rxjs ```javascript const fetchUserEpic = action$ => actions$ .ofType('fetch_user') .map(()=>({type:'fetch_user_ing'})) .flatMap(()=>{ return Observable .ajax .get('/api/user/1') .map(user => ({ type: 'fetch_user_done', user })) .takeUntil(action$.ofType('fetch_user_cancel')) }) ``` ---- > Do three things in sequence > Saga ```javascript function* worker1() { ... } function* worker2() { ... } function* worker3() { ... } function* watcher() { const score1 = yield* worker1() yield put(({type:'show_score', score1}) const score2 = yield* worker2() yield put(({type:'show_score', score2}) const score3 = yield* worker3() yield put(({type:'show_score', score3}) } ``` ---- > Do three things in sequence > Rxjs ```javascript const score1Epic = action$ => action$ .ofType('score1') .reduce(worker1) .flatMap(score=>{ return Observable.merge( {type:'show_score', score}, {type:'score2'} ) }) const score2Epic = action$ => action$ .ofType('score2') .reduce(worker2) .flatMap(score=>{ return Observable.merge( {type:'show_score', score}, {type:'score3'} ) }) const score3Epic = action$ => action$ .ofType('score3') .reduce(worker3) .map(score=>({type:'show_score', score})) ``` ---- > Login, token, Logout, Cancel > (with redux) ```javascript const store = { token: null, isFetching: false } const tokenReducer = (state=null, action) => { switch (action.type) { case 'login_success': return action.token case 'login_error': case 'login_cancel': case 'logout' return null default: return state } } const isFetching = (state=false, action) => { switch (action.type) { case 'login_request': return true case 'login_success': case 'login_error': case 'login_cancel': case 'logout' return false default: return state } } ``` ---- > Login, token, Logout, Cancel > Saga ```javascript import { take, put, call, fork, cancel } from 'redux-saga/effects' import Api from '...' function* loginWatcher() { const { user, password } = yield take('login_request') , task = yield fork(authorize, user, password) , action = yield take(['logout', 'login_error']) if (action.type === 'logout') { yield cancel(task) yield put({type:'login_cancel'}) } } function* authorize(user, password) { try { const token = yield call(Api.getUserToken, user, password) yield put({type: 'login_success', token}) } catch (error) { yield put({type: 'login_error', error}) } } ``` ---- > Login, token, Logout, Cancel > Rxjs ```javascript const authEpic = action$ => action$ .ofType('login_request') .flatMap(({payload:{user,password}})=> Observable .ajax .get('/api/userToken', { user, password }) .map(({token}) => ({ type: 'login_success', token })) .takeUntil(action$.ofType('login_cancel', 'logout')) .catch(error => Rx.Observable.of({type:'login_error', error})) ``` --- > Demo Part 2/3 ---- > Logger ```javascript // ----- Saga ----- \\ while (true) { const action = yield take('*') , state = yield select() console.log('action:', action) console.log('state:', state) } // ----- Rxjs ----- \\ .do(value=>console.log(value)) ``` ---- > Take latest request ```javascript // ----- Saga ----- \\ takeLatest() // ----- Rxjs ----- \\ .switchMap() ``` ---- > Retry with delay (1000ms) ```javascript // ----- Saga ----- \\ ... still thinking about it // ----- Rxjs ----- \\ .retryWhen(errors=>{ return errors.delay(1000).scan((errorCount, err)=>{ if(errorCount < 3) return errorCount + 1 throw err }, 0) }) ``` ---- > Error Handling ```javascript // ----- Saga ----- \\ try { // ... do things } catch (error) { yield put({ type:'fetch_user_error',error }) } // ----- Rxjs ----- \\ .catch(error => Observable.of({ type:'fetch_user_error', error })) ``` ---- > Running Tasks In Parallel ```javascript // ----- Saga ----- \\ import { call } from 'redux-saga/effects' const [users, repos] = yield [ call(fetch, '/users'), call(fetch, '/repos') ] // ----- Rxjs ----- \\ .flatMap(()=>{ return Observable.merge(promiseA, promiseB) }) ``` ---- > Throttling, Debouncing, Retrying > Saga ```javascript // ---- Throttling ---- \\ yield throttle(500, 'input_change', fun) // ---- Debouncing ---- \\ yield call(delay, 500) // ---- Retrying ---- \\ function* retryAPI(data) { for(let i = 0; i < 3; i++) { try { const apiResponse = yield call(apiRequest, { data }); return apiResponse; } catch(err) { if(i < 3) { yield call(delay, 2000); } } } throw new Error('API request failed'); } ``` ---- > Throttling, Debouncing, Retrying > Saga ```javascript // ---- Throttling ---- \\ .throtleTime(1000) // ---- Debouncing ---- \\ .debouncing(1000) // ---- Retrying ---- \\ .retry(3) ``` --- > Part (3/3) > Test ---- > Saga ```javascript { CALL: { fn: Api.fetch, <<<<<<<<<<<<< make sure call with the right fn args: ['./products'] <<<<<<<< make sure call with the right args } } expect( fetchUser('api/users/1').next().value ).to.eql( call(axios.get, 'api/users/1') ) ``` ---- > Rxjs ```javascript // fake store ... // ... // ... // https://redux-observable.js.org/docs/recipes/WritingTests.html ``` --- ### Which One Do I prefer? ---- > Coding Style #### Saga > 1. Effects as data > 2. (Imperative style) > Tell Program **HOW** to do things > Take time to reason about code #### Rxjs > 1. Events as data > 2. (Declarative Style) > Tell Program **WHAT** things to do > Easy to reason about code ---- > Function Reusability #### Saga > Low #### Rxjs > High ---- > Test #### Saga > * Unit Test(easy) > * Intergration Test(easy) > * Test might fail if the order change #### Rxjs > * Unit Test(Very easy) > * Intergration Test(Require Mocking) > * Test will not fail(same end result) ---- > Learn Saga > * Redux (1 day) > * Generator (1 day) > * Redux-Saga (few hours) > * Skill is not transferable > Total Learning Time: > > 3~5 days ---- > Learn Rxjs > * Redux (1 day) > * Rxjs (few days) > * Functional Programming (2 days) > * Redux-Observable (1 day) > * Skill is transferable > Total Learing Time: > > 1~2 weeks --- ### Futher Reading > merge() > map() > filter() > scan() > combineLatest() > concat() > do() > more ... > Visual Learning http://rxmarbles.com/ ---- ### Futher Discussion > Saga = **Effects** as data > Rxjs = **Events** as data > ++**Saga**++ doesn't actually perform the side effects itself. Instead, the helper functions will create objects that represents the intent of side effect. Then the ++**internal**++ library performs it for you". ```javascript call (http.get, '/users/1') // the task CALL: { fn: http.get, args: ['/users/1'] } // the intent ``` > This makes testing extremely easy, without the need for mocking. > ---- ###### tags: `React`, `Redux`, `Redux-Saga`, `Redux-Observable`