Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Why Redux-Observable?

Anna Su
November 04, 2017

Why Redux-Observable?

2017/11/04
@ 2017 JSDC

http://2017.jsdc.tw/agenda.html

Redux 提供了很好的狀態管理,把所有"修改狀態的 Side Effect" 透過不同的 Action Type 來區分,使我們可以很清楚追蹤狀態的變化歷程;但修改狀態以外的 Side Effect 一直存在著各種不同 Middleware 的解法,像是 Redux-thunk, Redux-promise 和 Redux-saga ... 等。本次分享將會說明 Redux-Observable 如何解決各種 side-effect 難題,以及為什麼我選擇 Redux-Observable。

Anna Su

November 04, 2017
Tweet

More Decks by Anna Su

Other Decks in Technology

Transcript

  1. 2

  2. 3

  3. 搜尋功能 你可能會這樣寫... 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; }); };
  4. 搜尋功能 搜尋關鍵字必須有值, 才執⾏行行 request 12 let controller; export const search

    = keyword !=> dispatch !=> { if (!keyword) { return; } dispatch({ type: SEARCH_REQUEST}); if (controller) { controller.abort(); }
  5. 搜尋功能 回傳搜尋資料之前, 顯⽰示 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,
  6. 搜尋功能 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; }); };
  7. 搜尋功能 觸發另外⼀一個 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; }); };
  8. 搜尋功能 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
  9. 搜尋功能 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
  10. 20

  11. 搜尋功能 宣告 controller 變數 21 let controller; export const search

    = keyword !=> dispatch !=> { if (!keyword) { return; } dispatch({ type: SEARCH_REQUEST}); if (controller) { controller.abort(); }
  12. 搜尋功能 新增 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; }); };
  13. 搜尋功能 將舊的 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; }); };
  14. 搜尋功能 使⽤用 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; }); };
  15. 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) ) );
  16. 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 ⼤大約
  17. 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 比較
  18. 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
  19. export const openToastEpic = action$ !=> { return action$ .ofType(OPEN_TOAST)

    .delay(3000) .mapTo({ type: CLOSE_TOAST }); }; Epic 40
  20. export const openToastEpic = action$ !=> { return action$ .ofType(OPEN_TOAST)

    .delay(3000) .mapTo({ type: CLOSE_TOAST }); }; Epic 41
  21. export const openToastEpic = action$ !=> { return action$ .ofType(OPEN_TOAST)

    .delay(3000) .mapTo({ type: CLOSE_TOAST }); }; Epic 42
  22. 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 );
  23. 49

  24. 53

  25. 訊息通知 建立 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 }); };
  26. 訊息通知 傳入 action observable 56 export const openToastEpic = action$

    !=> { return action$ .ofType(OPEN_TOAST) .delay(3000) .mapTo({ type: CLOSE_TOAST }); };
  27. 訊息通知 過濾出對應的 action type 57 export const openToastEpic = action$

    !=> { return action$ .ofType(OPEN_TOAST) .delay(3000) .mapTo({ type: CLOSE_TOAST }); };
  28. 訊息通知 delay 3 秒 58 export const openToastEpic = action$

    !=> { return action$ .ofType(OPEN_TOAST) .delay(3000) .mapTo({ type: CLOSE_TOAST }); };
  29. 訊息通知 觸發關閉 Toast 的 action 59 export const openToastEpic =

    action$ !=> { return action$ .ofType(OPEN_TOAST) .delay(3000) .mapTo({ type: CLOSE_TOAST }); };
  30. 訊息通知 到 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; } }
  31. 訊息通知 到 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; } }
  32. 66

  33. 67

  34. Email 即時驗證 export const checkEmailEpic = actions !=> { return

    actions .ofType(CHECK_EMAIL) .debounceTime(500) .switchMap(checkEmailIsUnique) .map(receiveEmailStatus); }; 過濾出對應的 action type 69
  35. Email 即時驗證 export const checkEmailEpic = actions !=> { return

    actions .ofType(CHECK_EMAIL) .debounceTime(500) .switchMap(checkEmailIsUnique) .map(receiveEmailStatus); }; 觸發後,靜置500毫秒 70
  36. Email 即時驗證 export const checkEmailEpic = actions !=> { return

    actions .ofType(CHECK_EMAIL) .debounceTime(500) .filter(validateEmail) .switchMap(checkEmailIsUnique) .map(receiveEmailStatus); }; 觸發後,靜置500毫秒才做驗證 71 通常⽤用在輸入框 靜置⼀一段時間才觸發 debounceTime
  37. Email 即時驗證 export const checkEmailEpic = actions !=> { return

    actions .ofType(CHECK_EMAIL) .debounceTime(500) .switchMap(checkEmailIsUnique) .map(receiveEmailStatus); }; 接收最新的 request, 舊的 request 都取消 72
  38. 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 的輸入框
  39. Email 即時驗證 export const checkEmailEpic = actions !=> { return

    actions .ofType(CHECK_EMAIL) .debounceTime(500) .switchMap(checkEmailIsUnique) .map(receiveEmailStatus); }; 獲取 Email 狀狀態 74
  40. Email 即時驗證 export const checkEmailEpic = actions !=> { return

    actions .ofType(CHECK_EMAIL) .debounceTime(500) .filter(validateEmail) .switchMap(checkEmailIsUnique) .map(receiveEmailStatus); }; Email 格式正確才發 request 76
  41. Email 即時驗證 export const checkEmailEpic = actions !=> { return

    actions .ofType(CHECK_EMAIL) .debounceTime(500) .filter(validateEmail) .switchMap(checkEmailIsUnique) .map(receiveEmailStatus); }; 觸發後,靜置500毫秒才做驗證 77 快速輕鬆的完成功能 不需要會 RxJS,就可以使⽤用 operator
  42. 79

  43. 過濾出對應的 action type 80 export const getArticlesEpic = action$ !=>

    { return action$ .ofType(GET_ARTICLES) .exhaustMap(getArticlesAPI) .map(receiveArticles); }; 載入更更多⽂文章
  44. 取原本送出的 request, 新的 request 都取消 81 export const getArticlesEpic =

    action$ !=> { return action$ .ofType(GET_ARTICLES) .exhaustMap(getArticlesAPI) .map(receiveArticles); }; 載入更更多⽂文章
  45. 83

  46. 無限滾動⽂文章 export const getArticlesEpic = action$ !=> { return action$

    .ofType(GET_ARTICLES) .exhaustMap(getArticlesAPI) .map(receiveArticles); }; 取原本送出的 request, 新的 request 都取消 84
  47. 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: 上傳檔案
  48. export const getArticlesEpic = actions !=> { return actions .ofType(GET_ARTICLES)

    .throttleTime(100) .exhaustMap(getArticlesAPI) .map(receiveArticles); }; 無限滾動⽂文章 throttle 100 毫秒 才送 request 86
  49. Email 即時驗證 export const checkEmailEpic = actions !=> { return

    actions .ofType(CHECK_EMAIL) .debounceTime(500) .filter(validateEmail) .switchMap(checkEmailIsUnique) .map(receiveEmailStatus); }; 觸發後,靜置500毫秒才做驗證 87 通常⽤用在連續性⾏行行為 throttleTime 情境1: 滾動事件 情境2: 拖拉事件 避免⾼高頻率觸發
  50. Email 即時驗證 export const checkEmailEpic = actions !=> { return

    actions .ofType(CHECK_EMAIL) .debounceTime(500) .filter(validateEmail) .switchMap(checkEmailIsUnique) .map(receiveEmailStatus); }; 觸發後,靜置500毫秒才做驗證 88 重⽤用性與可讀性提昇 組合使⽤用 operator
  51. Email 即時驗證 export const checkEmailEpic = actions !=> { return

    actions .ofType(CHECK_EMAIL) .debounceTime(500) .filter(validateEmail) .switchMap(checkEmailIsUnique) .map(receiveEmailStatus); }; 觸發後,靜置500毫秒才做驗證 89 優雅的解決非同步問題 簡單使⽤用 operator
  52. 91

  53. 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
  54. 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 }); };
  55. redux-thunk •容易易上⼿手 •程式碼冗長 •不易易閱讀 •難以維護 95 export const openPopup =

    () !=> dispatch !=> { dispatch({ type: OPEN_POPUP, }); setTimeout(() !=> { dispatch({ type: CLOSE_POPUP, }); }, 3000); };
  56. 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 }); };
  57. 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) } • 可以處理理較複雜的非同步問題 • 星星數較多,使⽤用社群較⼤大
  58. 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$ } }
  59. 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) );
  60. 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$, }; } •可以處理理較複雜的 非同步問題
  61. (你應該學習 Observable 的原因) 103 技術可轉移 RxJava RxPHP 將成為 ECMAScript 標準

    Stage1 前端框架都有 Observable Redux-Observable vue-rx Angular2 之後 RxSwift Why Redux-Observable?
  62. 105

  63. 106

  64. 107

  65. 108

  66. 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 ‣ https://en.wikipedia.org/wiki/Generator_(computer_programming) ‣ http://reactivex.io/languages.html 109