Saga = Worker + Watcher
Rxjs = Epic( Type + Operators )
Saga = Worker + Watcher
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 )
import API from '...'
const Epic = action$ =>
action$
.ofType('do_thing')
.flatMap(()=>API.get('/api/users'))
.map(users=>({type:'done', users}))
Demo Part (1/3)
Fetch User
Fetch User (cancelable)
Do three things in sequence
Login, Logout, Cancel (with redux)
Fetch User from /api/users/1
Saga
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
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
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
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
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
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)
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
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
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
// ----- 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
// ----- Saga ----- \\
takeLatest()
// ----- Rxjs ----- \\
.switchMap()
Retry with delay (1000ms)
// ----- 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
// ----- 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
// ----- 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
// ---- 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
// ---- Throttling ---- \\
.throtleTime(1000)
// ---- Debouncing ---- \\
.debouncing(1000)
// ---- Retrying ---- \\
.retry(3)
Part (3/3)
Test
Saga
{
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
// fake store ...
// ...
// ...
// https://redux-observable.js.org/docs/recipes/WritingTests.html
Coding Style
- Effects as data
- (Imperative style)
Tell Program HOW to do things
Take time to reason about code
- Events as data
- (Declarative Style)
Tell Program WHAT things to do
Easy to reason about code
Function Reusability
Low
High
Test
- Unit Test(easy)
- Intergration Test(easy)
- Test might fail if the order change
- 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
merge()
map()
filter()
scan()
combineLatest()
concat()
do()
more …
Visual Learning http://rxmarbles.com/
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".
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.
React
, Redux
, Redux-Saga
, Redux-Observable