Why Redux-Observable?
JSDC
2017/11/04
2
3
通常我們如何在 Redux 處理理
非同步問題?
5
Redux-thunk
6
Redux-thunk
redux 專案⼤大多使⽤用 redux-thunk 處理理非同步問題
為什什麼不繼續使⽤用
Redux-thunk
7
?
我們來來想想...
實務上我們可能會遇到哪些問題?
8
PM 跟你說:網站要做 搜尋 功能
9
‣ 顯⽰示搜尋結果
‣ 記錄關鍵字 History
如果使⽤用 redux-thunk
我們來來看程式碼可能會怎麼寫...
10
?
搜尋功能
你可能會這樣寫...
11
let controller;
export const search = keyword !=> dispatch !=> {
if (!keyword) {
return;
}
dispatch({ type: SEARCH_REQUEST});
if (controller) {
controller.abort();
}
controller = new AbortController();
return fetch(`/search?k=${keyword}`, {
signal: controller.signal,
}).then(res !=> {
dispatch(receiveSearchResult(res));
dispatch(push(`/search/${keyword}`));
dispatch(addKeywordHistory(keyword));
controller = undefined;
});
};
搜尋功能
搜尋關鍵字必須有值,
才執⾏行行 request
12
let controller;
export const search = keyword !=> dispatch !=> {
if (!keyword) {
return;
}
dispatch({ type: SEARCH_REQUEST});
if (controller) {
controller.abort();
}
搜尋功能
回傳搜尋資料之前,
顯⽰示 Loading 動畫
13
let controller;
export const search = keyword !=> dispatch !=> {
if (!keyword) {
return;
}
dispatch({ type: SEARCH_REQUEST });
if (controller) {
controller.abort();
}
controller = new AbortController();
return fetch(`/search?k=${keyword}`, {
signal: controller.signal,
搜尋功能
GET API,取得搜尋資料
14
dispatch({ type: SEARCH_REQUEST});
if (controller) {
controller.abort();
}
controller = new AbortController();
return fetch(`/search?k=${keyword}`, {
signal: controller.signal,
}).then(res !=> {
dispatch(receiveSearchResult(res));
dispatch(push(`/search/${keyword}`));
dispatch(addKeywordHistory(keyword));
controller = undefined;
});
};
搜尋功能
觸發另外⼀一個 action
顯⽰示搜尋結果
15
dispatch({ type: SEARCH_REQUEST});
if (controller) {
controller.abort();
}
controller = new AbortController();
return fetch(`/search?k=${keyword}`, {
signal: controller.signal,
}).then(res !=> {
dispatch(receiveSearchResult(res));
dispatch(push(`/search/${keyword}`));
dispatch(addKeywordHistory(keyword));
controller = undefined;
});
};
搜尋功能
dispatch({ type: SEARCH_REQUEST});
if (controller) {
controller.abort();
}
controller = new AbortController();
return fetch(`/search?k=${keyword}`, {
signal: controller.signal,
}).then(res !=> {
dispatch(receiveSearchResult(res));
dispatch(push(`/search/${keyword}`));
dispatch(addKeywordHistory(keyword));
controller = undefined;
});
};
變更更網址
16
搜尋功能
dispatch({ type: SEARCH_REQUEST});
if (controller) {
controller.abort();
}
controller = new AbortController();
return fetch(`/search?k=${keyword}`, {
signal: controller.signal,
}).then(res !=> {
dispatch(receiveSearchResult(res));
dispatch(push(`/search/${keyword}`));
dispatch(addKeywordHistory(keyword));
controller = undefined;
});
};
傳入關鍵字,記錄 History
17
如何在 Redux-thunk
取消 request?
18
如何在 Redux-thunk
取消 request?
19
20
搜尋功能
宣告 controller 變數
21
let controller;
export const search = keyword !=> dispatch !=> {
if (!keyword) {
return;
}
dispatch({ type: SEARCH_REQUEST});
if (controller) {
controller.abort();
}
搜尋功能
新增 controller
提供取消 request 的⽅方法
22
dispatch({ type: SEARCH_REQUEST});
if (controller) {
controller.abort();
}
controller = new AbortController();
return fetch(`/search?keyword=${keyword}&lim
signal: controller.signal,
}).then(res !=> {
dispatch(receiveSearchResult(res));
dispatch(push(`/search/${keyword}`));
dispatch(addKeywordHistory(keyword));
controller = undefined;
});
};
搜尋功能
將舊的 request 取消,
執⾏行行新的 request
23
dispatch({ type: SEARCH_REQUEST});
if (controller) {
controller.abort();
}
controller = new AbortController();
return fetch(`/search?keyword=${keyword}&lim
signal: controller.signal,
}).then(res !=> {
dispatch(receiveSearchResult(res));
dispatch(push(`/search/${keyword}`));
dispatch(addKeywordHistory(keyword));
controller = undefined;
});
};
搜尋功能
使⽤用 redux-thunk…
24
let controller;
export const search = keyword !=> dispatch !=> {
if (!keyword) {
return;
}
dispatch({ type: SEARCH_REQUEST});
if (controller) {
controller.abort();
}
controller = new AbortController();
return fetch(`/search?keyword=${keyword}&limit=20`, {
signal: controller.signal,
}).then(res !=> {
dispatch(receiveSearchResult(res));
dispatch(push(`/search/${keyword}`));
dispatch(addKeywordHistory(keyword));
controller = undefined;
});
};
25
有沒有更更快速⼜又簡單的⽅方法?
幫我們解決這些常⾒見見的非同步問題
26
redux-thunk redux-observable
let controller;
export const search = keyword !=> dispatch !=> {
if (!keyword) {
return;
}
dispatch({ type: SEARCH_REQUEST});
if (controller) {
controller.abort();
}
controller = new AbortController();
return fetch(`/search?k=${keyword}`, {
signal: controller.signal,
}).then(res !=> {
dispatch(receiveSearchResult(res));
dispatch(push(`/search/${keyword}`));
dispatch(addKeywordHistory(keyword));
controller = undefined;
});
};
export const searchEpic = action$ !=>
action$
.ofType(SEARCH_REQUEST)
.filter(action !=> action.payload)
.switchMap(getSearchResult)
.mergeMap(res !=>
Observable.of(
receiveSearchResult(res.data),
push(`/search/${res.keyword}`),
addSearchHistory(res.keyword)
)
);
27
let controller;
export const search = keyword !=> dispatch !=> {
if (!keyword) {
return;
}
dispatch({ type: SEARCH_REQUEST});
if (controller) {
controller.abort();
}
controller = new AbortController();
return fetch(`/search?k=${keyword}`, {
signal: controller.signal,
}).then(res !=> {
dispatch(receiveSearchResult(res));
dispatch(push(`/search/${keyword}`));
dispatch(addKeywordHistory(keyword));
controller = undefined;
});
};
export const searchEpic = action$ !=>
action$
.ofType(SEARCH_REQUEST)
.filter(action !=> action.payload)
.switchMap(getSearchResult)
.mergeMap(res !=>
Observable.of(
receiveSearchResult(res.data),
push(`/search/${res.keyword}`),
addSearchHistory(res.keyword)
)
);
25⾏行行程式碼 12⾏行行程式碼
程式碼 少了了 1/2
redux-observable
⼤大約
28
let controller;
export const search = keyword !=> dispatch !=> {
if (!keyword) {
return;
}
dispatch({ type: SEARCH_REQUEST});
if (controller) {
controller.abort();
}
controller = new AbortController();
return fetch(`/search?k=${keyword}`, {
signal: controller.signal,
}).then(res !=> {
dispatch(receiveSearchResult(res));
dispatch(push(`/search/${keyword}`));
dispatch(addKeywordHistory(keyword));
controller = undefined;
});
};
export const searchEpic = action$ !=>
action$
.ofType(SEARCH_REQUEST)
.filter(action !=> action.payload)
.switchMap(getSearchResult)
.mergeMap(res !=>
Observable.of(
receiveSearchResult(res.data),
push(`/search/${res.keyword}`),
addSearchHistory(res.keyword)
)
);
有 if 判斷 沒有 if 判斷
程式碼 容易易閱讀
redux-observable
比較
29
let controller;
export const search = keyword !=> dispatch !=> {
if (!keyword) {
return;
}
dispatch({ type: SEARCH_REQUEST});
if (controller) {
controller.abort();
}
controller = new AbortController();
return fetch(`/search?k=${keyword}`, {
signal: controller.signal,
}).then(res !=> {
dispatch(receiveSearchResult(res));
dispatch(push(`/search/${keyword}`));
dispatch(addKeywordHistory(keyword));
controller = undefined;
});
};
export const searchEpic = action$ !=>
action$
.ofType(SEARCH_REQUEST)
.filter(action !=> action.payload)
.switchMap(getSearchResult)
.mergeMap(res !=>
Observable.of(
receiveSearchResult(res.data),
push(`/search/${res.keyword}`),
addSearchHistory(res.keyword)
)
);
只⽀支援 firefox 57 版 只有 IE 10 以下不⽀支援
有效率的完成功能
redux-observable
30
好像還不錯?!
Redux-Observable
Redux-Observable
What’s
Redux-Observable?
RxJS 5-based middleware for Redux
32
33
Store
dispatch
Reducer
Action
Middleware
Component
State
34
Store
dispatch
Reducer
Action
Middleware
Component
State
35
Store
dispatch
Reducer
Action
Middleware
Component
State
36
Store
dispatch
Reducer
Action
Middleware
Component
State
37
Store
dispatch
epic epic epic epicepic
redux-observable
Reducer
Action
State
38
Store
dispatch
epic epic epic epicepic
redux-observable
Reducer
Action
State
39
Store
dispatch
epic epic epic epicepic
redux-observable
Reducer
Action
State
export const openToastEpic = action$ !=> {
return action$
.ofType(OPEN_TOAST)
.delay(3000)
.mapTo({ type: CLOSE_TOAST });
};
Epic
40
export const openToastEpic = action$ !=> {
return action$
.ofType(OPEN_TOAST)
.delay(3000)
.mapTo({ type: CLOSE_TOAST });
};
Epic
41
export const openToastEpic = action$ !=> {
return action$
.ofType(OPEN_TOAST)
.delay(3000)
.mapTo({ type: CLOSE_TOAST });
};
Epic
42
43
epic epic epic epicepic
epic epic epic epicepic
rootEpic
44
rootEpic.js
45
import { combineEpics } from 'redux-observable';
import { openToastEpic } from './toastStatus.js';
import { checkEmailEpic } from './emailCheckStatus.js';
import { searchEpic } from './search.js';
import { getArticlesEpic } from './articles.js';
export default combineEpics(
openToastEpic,
checkEmailEpic,
searchEpic,
getArticlesEpic
);
index.js
46
…
const store = createStore(
reducer,
applyMiddleware(createEpicMiddleware(rootEpics))
);
…
Store
Middleware
Reducer
index.js
47
…
const store = createStore(
reducer,
applyMiddleware(createEpicMiddleware(rootEpics))
);
…
Store
Middleware
Reducer
index.js
48
…
const store = createStore(
reducer,
applyMiddleware(createEpicMiddleware(rootEpics))
);
…
Store
Middleware
Reducer
49
50
訊息通知
51
按下確定按鈕
52
open toast
53
訊息通知
按下按鈕
觸發 action creator
54
<button onClick={openToast}>確定#</button>
訊息通知
建立 action creator 和 epic
55
export const openToast = () !=> ({
type: OPEN_TOAST,
});
export const openToastEpic = action$ !=> {
return action$
.ofType(OPEN_TOAST)
.delay(3000)
.mapTo({ type: CLOSE_TOAST });
};
訊息通知
傳入 action observable
56
export const openToastEpic = action$ !=> {
return action$
.ofType(OPEN_TOAST)
.delay(3000)
.mapTo({ type: CLOSE_TOAST });
};
訊息通知
過濾出對應的 action type
57
export const openToastEpic = action$ !=> {
return action$
.ofType(OPEN_TOAST)
.delay(3000)
.mapTo({ type: CLOSE_TOAST });
};
訊息通知
delay 3 秒
58
export const openToastEpic = action$ !=> {
return action$
.ofType(OPEN_TOAST)
.delay(3000)
.mapTo({ type: CLOSE_TOAST });
};
訊息通知
觸發關閉 Toast 的 action
59
export const openToastEpic = action$ !=> {
return action$
.ofType(OPEN_TOAST)
.delay(3000)
.mapTo({ type: CLOSE_TOAST });
};
訊息通知
到 reducer、更更新 state
改變 store
60
export default function toastStatus(state = false, action) {
switch (action.type) {
case OPEN_TOAST:
return true;
case CLOSE_TOAST:
return false;
default:
return state;
}
}
訊息通知
到 reducer、更更新 state
改變 store
61
export default function toastStatus(state = false, action) {
switch (action.type) {
case OPEN_TOAST:
return true;
case CLOSE_TOAST:
return false;
default:
return state;
}
}
訊息通知
UI 發⽣生改變
62
<div className={classNames('toast', { show })}>
<p>儲存成功!#</p>
#</div>
Redux-Observable
use case
64
E-mail 即時驗證 載入更更多⽂文章
Redux-Observable use case
65
E-mail 即時驗證
66
67
68
取消舊的 request,執⾏行行新的 request
Email 即時驗證
export const checkEmailEpic = actions !=> {
return actions
.ofType(CHECK_EMAIL)
.debounceTime(500)
.switchMap(checkEmailIsUnique)
.map(receiveEmailStatus);
};
過濾出對應的 action type
69
Email 即時驗證
export const checkEmailEpic = actions !=> {
return actions
.ofType(CHECK_EMAIL)
.debounceTime(500)
.switchMap(checkEmailIsUnique)
.map(receiveEmailStatus);
};
觸發後,靜置500毫秒
70
Email 即時驗證
export const checkEmailEpic = actions !=> {
return actions
.ofType(CHECK_EMAIL)
.debounceTime(500)
.filter(validateEmail)
.switchMap(checkEmailIsUnique)
.map(receiveEmailStatus);
};
觸發後,靜置500毫秒才做驗證
71
通常⽤用在輸入框
靜置⼀一段時間才觸發
debounceTime
Email 即時驗證
export const checkEmailEpic = actions !=> {
return actions
.ofType(CHECK_EMAIL)
.debounceTime(500)
.switchMap(checkEmailIsUnique)
.map(receiveEmailStatus);
};
接收最新的 request,
舊的 request 都取消
72
Email 即時驗證
export const checkEmailEpic = actions !=> {
return actions
.ofType(CHECK_EMAIL)
.debounceTime(500)
.filter(validateEmail)
.switchMap(checkEmailIsUnique)
.map(receiveEmailStatus);
};
取最新的 request,
舊的 request 都取消
73
取消舊的 request,執⾏行行新的 request
switchMap
情境1: 表單即時驗證
情境2: Autocomplete 的輸入框
Email 即時驗證
export const checkEmailEpic = actions !=> {
return actions
.ofType(CHECK_EMAIL)
.debounceTime(500)
.switchMap(checkEmailIsUnique)
.map(receiveEmailStatus);
};
獲取 Email 狀狀態
74
75
ㄈ
Email 即時驗證
export const checkEmailEpic = actions !=> {
return actions
.ofType(CHECK_EMAIL)
.debounceTime(500)
.filter(validateEmail)
.switchMap(checkEmailIsUnique)
.map(receiveEmailStatus);
};
Email 格式正確才發 request
76
Email 即時驗證
export const checkEmailEpic = actions !=> {
return actions
.ofType(CHECK_EMAIL)
.debounceTime(500)
.filter(validateEmail)
.switchMap(checkEmailIsUnique)
.map(receiveEmailStatus);
};
觸發後,靜置500毫秒才做驗證
77
快速輕鬆的完成功能
不需要會 RxJS,就可以使⽤用 operator
78
載入更更多⽂文章
79
過濾出對應的 action type
80
export const getArticlesEpic = action$ !=> {
return action$
.ofType(GET_ARTICLES)
.exhaustMap(getArticlesAPI)
.map(receiveArticles);
};
載入更更多⽂文章
取原本送出的 request,
新的 request 都取消
81
export const getArticlesEpic = action$ !=> {
return action$
.ofType(GET_ARTICLES)
.exhaustMap(getArticlesAPI)
.map(receiveArticles);
};
載入更更多⽂文章
82
無限滾動⽂文章
83
無限滾動⽂文章
export const getArticlesEpic = action$ !=> {
return action$
.ofType(GET_ARTICLES)
.exhaustMap(getArticlesAPI)
.map(receiveArticles);
};
取原本送出的 request,
新的 request 都取消
84
Email 即時驗證
export const checkEmailEpic = actions !=> {
return actions
.ofType(CHECK_EMAIL)
.debounceTime(500)
.filter(validateEmail)
.switchMap(checkEmailIsUnique)
.map(receiveEmailStatus);
};
取最新的 request,
舊的 request 都取消
85
取原本送出的 request,新的 request 都取消
exhaustMap
情境1: 看更更多⽂文章
情境2: 上傳檔案
export const getArticlesEpic = actions !=> {
return actions
.ofType(GET_ARTICLES)
.throttleTime(100)
.exhaustMap(getArticlesAPI)
.map(receiveArticles);
};
無限滾動⽂文章
throttle 100 毫秒
才送 request
86
Email 即時驗證
export const checkEmailEpic = actions !=> {
return actions
.ofType(CHECK_EMAIL)
.debounceTime(500)
.filter(validateEmail)
.switchMap(checkEmailIsUnique)
.map(receiveEmailStatus);
};
觸發後,靜置500毫秒才做驗證
87
通常⽤用在連續性⾏行行為
throttleTime
情境1: 滾動事件
情境2: 拖拉事件
避免⾼高頻率觸發
Email 即時驗證
export const checkEmailEpic = actions !=> {
return actions
.ofType(CHECK_EMAIL)
.debounceTime(500)
.filter(validateEmail)
.switchMap(checkEmailIsUnique)
.map(receiveEmailStatus);
};
觸發後,靜置500毫秒才做驗證
88
重⽤用性與可讀性提昇
組合使⽤用 operator
Email 即時驗證
export const checkEmailEpic = actions !=> {
return actions
.ofType(CHECK_EMAIL)
.debounceTime(500)
.filter(validateEmail)
.switchMap(checkEmailIsUnique)
.map(receiveEmailStatus);
};
觸發後,靜置500毫秒才做驗證
89
優雅的解決非同步問題
簡單使⽤用 operator
你不需要會 RxJS,
就可以直接使⽤用 Redux-Observable !
90
91
Compare with
other middleware ?
93
13 Jul 2015
redux-thunk redux-saga
3 Dec 2015
redux-observable
21 Apr 2016
redux-cycle
3 Dec 2016
2017/10/23
Redux middlewares
redux-thunk redux-observable
94
export const openPopup = () !=> dispatch !=> {
dispatch({
type: OPEN_POPUP,
});
setTimeout(() !=> {
dispatch({
type: CLOSE_POPUP,
});
}, 3000);
};
export const openPopupEpic = action$ !=> {
return action$
.ofType(OPEN_POPUP)
.delay(3000)
.mapTo({ type: CLOSE_POPUP });
};
redux-thunk
•容易易上⼿手
•程式碼冗長
•不易易閱讀
•難以維護
95
export const openPopup = () !=> dispatch !=> {
dispatch({
type: OPEN_POPUP,
});
setTimeout(() !=> {
dispatch({
type: CLOSE_POPUP,
});
}, 3000);
};
redux-saga redux-observable
96
export function* openPopupAsync () {
yield call(delay, 3000)
yield put({ type: 'CLOSE_POPUP' })
}
export function* watchOpenPopupAsync () {
yield takeEvery('OPEN_POPUP', openPopupAsync)
}
export const openPopupEpic = action$ !=> {
return action$
.ofType(OPEN_POPUP)
.delay(3000)
.mapTo({ type: CLOSE_POPUP });
};
redux-saga
•技術不能轉移
•依賴 syntax (Generator)
97
export function* openPopupAsync () {
yield call(delay, 3000)
yield put({ type: 'CLOSE_POPUP' })
}
export function* watchOpenPopupAsync () {
yield takeEvery('OPEN_POPUP', openPopupAsync)
}
•可以處理理較複雜的非同步問題
•星星數較多,使⽤用社群較⼤大
redux-cycles redux-observable
98
export const openPopupEpic = action$ !=> {
return action$
.ofType(OPEN_POPUP)
.delay(3000)
.mapTo({ type: CLOSE_POPUP });
};
function openPopup (sources) {
const openPopup$ = sources.ACTION
.filter(action !=> action.type &&=== OPEN_POPUP)
.delay(3000)
.mapTo({ type: CLOSE_POPUP });
return {
ACTION: openPopup$
}
}
redux-cycles redux-observable
99
function fetchUserData(sources) {
const request$ = sources.ACTION
.filter(action !=> action.type &&=== FETCH_USER)
.map(action !=> ({
url: `${API_URL}users/`,
category: 'users',
}));
const action$ = sources.HTTP
.select('users')
.flatten()
.map(fetchUserFulfilled);
return {
ACTION: action$,
HTTP: request$,
};
}
const fetchUserDataEpic = action$ !=>
action$
.ofType(FETCH_USER)
.mergeMap(action !=>
ajax.getJSON(`${API_URL}users/`)
.map(fetchUserFulfilled)
);
redux-cycle
•社群⼈人數較少
•過度封裝?
100
function fetchUserData(sources) {
const request$ = sources.ACTION
.filter(action !=> action.type &&=== FETCH_USER)
.map(action !=> ({
url: `${API_URL}users/`,
category: 'users',
}));
const action$ = sources.HTTP
.select('users')
.flatten()
.map(fetchUserFulfilled);
return {
ACTION: action$,
HTTP: request$,
};
}
•可以處理理較複雜的
非同步問題
101
redux-thunk redux-saga redux-cycle redux-observable
程式碼簡潔
處理複雜的⾮同步情境
技術可轉移
Why Redux-Observable?
102
redux-thunk redux-saga redux-cycle redux-observable
程式碼簡潔
處理複雜的⾮同步情境
技術可轉移
Why Redux-Observable?
(你應該學習 Observable 的原因)
103
技術可轉移
RxJava
RxPHP
將成為 ECMAScript 標準
Stage1
前端框架都有 Observable
Redux-Observable
vue-rx
Angular2 之後RxSwift
Why Redux-Observable?
104
‣ 程式碼更更簡潔
‣ 可讀性更更⾼高
‣ 容易易寫測試
‣ 更更快速完成功能
Why Redux-Observable?
105
106
107
108
Reference
‣ https://redux-observable.js.org/
‣ https://github.com/redux-observable/redux-observable
‣ http://reactivex.io/languages.html
‣ https://github.com/reactjs/redux/issues/1461#issuecomment-190165193
‣ https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort
‣ https://twitter.com/dan_abramov/status/816244945015160832
109
Image Credit
‣ http://oreilly-generator.com/
‣ http://renzhou.tw/yinwubrother-textmaker/
‣ https://www.flaticon.com/packs/emoji-6
‣ https://www.flaticon.com/packs/design-35
110
Thank you :D

Why Redux-Observable?