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`