Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

RxJS + Redux + React = Amazing

28,912 views

Published on

Manage your redux async/side effects with RxJS using redux-observable middleware. Applies not just to React, also Angular2, Ember, etc.

Published in: Software

RxJS + Redux + React = Amazing

  1. 1. RxJS + Redux + React = Amazing Jay Phelps | @_jayphelps Side Effect Management with RxJS
  2. 2. Jay Phelps | @_jayphelps Managing state stuff is hard
  3. 3. Jay Phelps | @_jayphelps Redux makes it simple (not necessarily easy)
  4. 4. Jay Phelps | @_jayphelps Managing async stuff is harder
  5. 5. Jay Phelps | @_jayphelps Some async is complex regardless of the abstraction
  6. 6. Jay Phelps | @_jayphelps RxJS makes it manageable
  7. 7. Jay Phelps Senior Software Engineer | @_jayphelps
  8. 8. What is redux? Jay Phelps | @_jayphelps
  9. 9. Crash Course Jay Phelps | @_jayphelps
  10. 10. What is redux? Jay Phelps | @_jayphelps Provides predicable state management using actions and reducers
  11. 11. What's an "action"? Jay Phelps | @_jayphelps Describes something has (or should) happen, but they don't specify how it should be done
  12. 12. Jay Phelps | @_jayphelps { type: 'CREATE_TODO', payload: 'Build my first Redux app' }
  13. 13. What's an "reducer"? Jay Phelps | @_jayphelps A pure function that takes the previous state and an action and returns the new state
  14. 14. What's an "reducer"? Jay Phelps | @_jayphelps Sometimes it returns the previous state (state, action) => state
  15. 15. What's an "reducer"? Jay Phelps | @_jayphelps Sometimes it computes new state (state, action) => state + action.payload
  16. 16. Jay Phelps | @_jayphelps const counter = (state = 0, action) => { switch (action.type) { case 'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } };
  17. 17. Jay Phelps | @_jayphelps Reducers handle state transitions, but they must be done synchronously.
  18. 18. Jay Phelps | @_jayphelps const counter = (state = 0, action) => { switch (action.type) { case 'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } };
  19. 19. Jay Phelps | @_jayphelps What are async stuff do we commonly do?
  20. 20. Async Jay Phelps | @_jayphelps • User interactions (mouse, keyboard, etc) • AJAX • Timers/Animations • Web Sockets • Work Workers, etc
  21. 21. Jay Phelps | @_jayphelps Some can be handled synchronously
  22. 22. Jay Phelps | @_jayphelps <button onClick={() => dispatch({ type: 'INCREMENT' })}> Increment </button>
  23. 23. Jay Phelps | @_jayphelps const counter = (state = 0, action) => { switch (action.type) { case 'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } };
  24. 24. Jay Phelps | @_jayphelps Sometimes you need more control
  25. 25. Jay Phelps | @_jayphelps • AJAX cancellation/composing • Debounce/throttle/buffer/etc • Drag and Drop • Web Sockets, Work Workers, etc
  26. 26. Jay Phelps | @_jayphelps Use middleware to manage async / side effects
  27. 27. Jay Phelps | @_jayphelps Most of them use callbacks or Promises
  28. 28. Callbacks Jay Phelps | @_jayphelps The most primitive way to handle async in JavaScript
  29. 29. Callbacks Jay Phelps | @_jayphelps fetchSomeData((error, data) => { if (!error) { dispatch({ type: 'HERES_THE_DATA', data }); } });
  30. 30. Callback Hell Jay Phelps | @_jayphelps fetchSomeData(id, (error, data) => { if (!error) { dispatch({ type: 'HERES_THE_FIRST_CALL_DATA', data }); fetchSomeData(data.parentId, (error, data) => { if (!error) { dispatch({ type: 'HERES_SOME_MORE', data }); fetchSomeData(data.parentId, (error, data) => { if (!error) { dispatch({ type: 'OMG_MAKE_IT_STOP', data }); } }); } }); } });
  31. 31. Promises Jay Phelps | @_jayphelps fetchSomeData(id) .then(data => { dispatch({ type: 'HERES_THE_FIRST_CALL_DATA', data }); return fetchSomeData(data.parentId); }) .then(data => { dispatch({ type: 'HERES_SOME_MORE', data }); return fetchSomeData(data.parentId); }) .then(data => { dispatch({ type: 'OKAY_IM_DONE', data }); });
  32. 32. Promises Jay Phelps | @_jayphelps • Guaranteed future • Immutable • Single value • Caching
  33. 33. Promises Jay Phelps | @_jayphelps • Guaranteed future • Immutable • Single value • Caching
  34. 34. Promises Jay Phelps | @_jayphelps • Guaranteed future • Immutable • Single value • Caching
  35. 35. Jay Phelps | @_jayphelps Promises cannot be cancelled
  36. 36. Jay Phelps | @_jayphelps Why would you want to cancel?
  37. 37. Jay Phelps | @_jayphelps • Changing routes/views • Auto-complete • User wants you to
  38. 38. Jay Phelps | @_jayphelps • Changing routes/views • Auto-complete • User wants you to
  39. 39. Jay Phelps | @_jayphelps Daredevil
  40. 40. Jay Phelps | @_jayphelps Daredevil The Get Down Here’s Daredevil!
  41. 41. Jay Phelps | @_jayphelps Daredevil
  42. 42. Jay Phelps | @_jayphelps Daredevil The Get Down
  43. 43. Jay Phelps | @_jayphelps Cancelling is common and often overlooked
  44. 44. Promises Jay Phelps | @_jayphelps • Guaranteed future • Immutable • Single value • Caching
  45. 45. Only AJAX is single value Jay Phelps | @_jayphelps • User interactions (mouse, keyboard, etc • AJAX • Animations • WebSockets, Workers, etc
  46. 46. Jay Phelps | @_jayphelps What do we use?
  47. 47. Jay Phelps | @_jayphelps Observables
  48. 48. Observables Jay Phelps | @_jayphelps • Stream of zero, one, or more values • Over any amount of time • Cancellable
  49. 49. Jay Phelps | @_jayphelps Streams are a set, with a dimension of time
  50. 50. Jay Phelps | @_jayphelps Being standardized for ECMAScript aka JavaScript
  51. 51. RxJS Jay Phelps | @_jayphelps “lodash for async” - Ben Lesh
  52. 52. Crash Course Jay Phelps | @_jayphelps
  53. 53. Creating Observables Jay Phelps | @_jayphelps • of('hello') • from([1, 2, 3, 4]) • interval(1000) • ajax('http://example.com') • webSocket('ws://echo.websocket.com') • fromEvent(button, ‘click') • many more…
  54. 54. Subscribing Jay Phelps | @_jayphelps myObservable.subscribe( value => console.log('next', value) );
  55. 55. Subscribing Jay Phelps | @_jayphelps myObservable.subscribe( value => console.log('next', value), err => console.error('error', err) );
  56. 56. Subscribing Jay Phelps | @_jayphelps myObservable.subscribe( value => console.log('next', value), err => console.error('error', err), () => console.info('complete!') );
  57. 57. Observables can be transformed Jay Phelps | @_jayphelps map, filter, reduce
  58. 58. Observables can be combined Jay Phelps | @_jayphelps concat, merge, zip
  59. 59. Observables represent time Jay Phelps | @_jayphelps debounce, throttle, buffer, combineLatest
  60. 60. Observables are lazy Jay Phelps | @_jayphelps retry, repeat
  61. 61. Jay Phelps | @_jayphelps Observables can represent just about anything
  62. 62. Jay Phelps | @_jayphelps Let’s combine RxJS and Redux!
  63. 63. Jay Phelps | @_jayphelps
  64. 64. Side effect management for redux, using Epics
  65. 65. What is an Epic? Jay Phelps | @_jayphelps A function that takes a stream of all actions dispatched and returns a stream of new actions to dispatch
  66. 66. Jay Phelps | @_jayphelps “actions in, actions out”
  67. 67. // This is pseudo code, not real function pingPong(action, store) { if (action.type === 'PING') { return { type: 'PONG' }; } } Jay Phelps | @_jayphelps Sort of like this
  68. 68. function pingPongEpic(action$, store) { return action$.ofType('PING') .map(action => ({ type: 'PONG' })); } An Epic Jay Phelps | @_jayphelps
  69. 69. const pingPongEpic = (action$, store) => action$.ofType('PING') .map(action => ({ type: 'PONG' })); An Epic Jay Phelps | @_jayphelps
  70. 70. An Epic Jay Phelps | @_jayphelps const pingPongEpic = (action$, store) => action$.ofType('PING') .delay(1000) // <— that's it .map(action => ({ type: 'PONG' }));
  71. 71. Jay Phelps | @_jayphelps const isPinging = (state = false, action) => { switch (action.type) { case 'PING': return true; case 'PONG': return false; default: return state; } };
  72. 72. const pingPongEpic = (action$, store) => action$.ofType('PING') .delay(1000) .map(action => ({ type: 'PONG' })); Jay Phelps | @_jayphelps
  73. 73. Jay Phelps | @_jayphelps Debounced increment / decrement button
  74. 74. Jay Phelps | @_jayphelps const counter = (state = 0, action) => { switch (action.type) { case 'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } };
  75. 75. Jay Phelps | @_jayphelps const incrementEpic = (action$, store) => action$.ofType('INCREMENT_DEBOUNCED') .debounceTime(1000) .map(() => ({ type: 'INCREMENT' })); const decrementEpic = (action$, store) => action$.ofType('DECREMENT_DEBOUNCED') .debounceTime(1000) .map(() => ({ type: 'DECREMENT' }));
  76. 76. Jay Phelps | @_jayphelps const incrementEpic = (action$, store) => action$.ofType('INCREMENT_DEBOUNCED') .debounceTime(1000) .map(() => ({ type: 'INCREMENT' })); const decrementEpic = (action$, store) => action$.ofType('DECREMENT_DEBOUNCED') .debounceTime(1000) .map(() => ({ type: 'DECREMENT' }));
  77. 77. Jay Phelps | @_jayphelps Those are contrived examples, obviously
  78. 78. Jay Phelps | @_jayphelps Warning: non-trivial examples ahead, don’t struggle to read them entirely
  79. 79. Jay Phelps | @_jayphelps Auto-complete
  80. 80. Jay Phelps | @_jayphelps onKeyUp(e) { const { store } = this.props; const { value } = e.target.value; if (this.queryId) { clearTimeout(this.queryId); } this.queryId = setTimeout(() => { if (this.xhr) { this.xhr.abort(); } const xhr = this.xhr = new XMLHttpRequest(); xhr.open('GET', 'https://api.github.com/search/users?q=' + value); xhr.onload = () => { if (xhr.status === 200) { store.dispatch({ type: 'QUERY_FULFILLED', payload: JSON.parse(xhr.response).items }); } else { store.dispatch({ type: 'QUERY_REJECTED', error: true, payload: { message: xhr.response, status: xhr.status } }); } }; xhr.send(); }, 500); } Plain JS
  81. 81. Jay Phelps | @_jayphelps Epic const autoCompleteEpic = (action$, store) => action$.ofType('QUERY') .debounceTime(500) .switchMap(action => ajax('https://api.github.com/search/users?q=' + value) .map(payload => ({ type: 'QUERY_FULFILLED', payload })) );
  82. 82. Jay Phelps | @_jayphelps Epic const autoCompleteEpic = (action$, store) => action$.ofType('QUERY') .debounceTime(500) .switchMap(action => ajax('https://api.github.com/search/users?q=' + value) .map(payload => ({ type: 'QUERY_FULFILLED', payload })) .catch(payload => [{ type: 'QUERY_REJECTED', error: true, payload }]) );
  83. 83. Jay Phelps | @_jayphelps Epic const autoCompleteEpic = (action$, store) => action$.ofType('QUERY') .debounceTime(500) .switchMap(action => ajax('https://api.github.com/search/users?q=' + value) .map(payload => ({ type: 'QUERY_FULFILLED', payload })) .takeUntil(action$.ofType('CANCEL_QUERY')) .catch(payload => [{ type: 'QUERY_REJECTED', error: true, payload }]) );
  84. 84. Jay Phelps | @_jayphelps OK, show me really non-trivial examples
  85. 85. Jay Phelps | @_jayphelps Bidirectional, multiplexed Web Sockets
  86. 86. Jay Phelps | @_jayphelps class Example { @autobind checkChange(e) { const { value: key, checked } = e.target; this.subs = this.subs || []; if (checked) { const handler = e => { const data = JSON.parse(e.data); if (data.key === key) { this.updateValue(key, data.value); } }; this.subs.push({ key, handler }) const socket = this.getSocket(() => { this.setState({ socketOpen: true }); this.subs.forEach(({ key }) => socket.send(JSON.stringify({ type: 'sub', key}))); }); socket.addEventListener('message', handler); } else { const index = this.subs.findIndex(x => x.key === key); if (index !== -1) { this.subs.splice(index, 1); } const { socket } = this; if (socket && socket.readyState === 1) { socket.send(JSON.stringify({ type: 'unsub', key })); this.setInactive(key)l if (this.subs.length === 0) { socket.close(); } } } } componentWillUnMount() { if (this.socket && this.socket.readyState === 1) { this.socket.close(); } } Plain JS getSocket(callback) { const { socket } = this; if (socket && socket.readyState === 1) { setTimeot(callback); } else { if (this.reconnectId) { clearTimeout(this.reconnectId); } socket = this.socket = new WebSocket(‘ws://localhost:3000'); socket.onopen = () => { callback(); }; socket.onerror = () => { this.reconnectId = setTimeout(() => this.getSocket(callback), 1000); this.setState({ socketOpen: false }); }; socket.onclose = (e) => { if (!e.wasClean) { this.reconnectId = setTimeout(() => this.getSocket(callback), 1000); } this.setState({ socketOpen: false }); }; } return socket; } }
  87. 87. Jay Phelps | @_jayphelps Too much code
  88. 88. Jay Phelps | @_jayphelps As an Epic const socket = WebSocketSubject.create('ws://stock/endpoint'); const stockTickerEpic = (action$, store) => action$.ofType('START_TICKER_STREAM') .mergeMap(action => socket.multiplex( () => ({ sub: action.ticker }), () => ({ unsub: action.ticker }), msg => msg.ticker === action.ticker ) .retryWhen( err => window.navigator.onLine ? Observable.timer(1000) : Observable.fromEvent(window, 'online') ) .takeUntil( action$.ofType('CLOSE_TICKER_STREAM') .filter(closeAction => closeAction.ticker === action.ticker) ) .map(tick => ({ type: 'TICKER_TICK', tick })) );
  89. 89. redux-observable Jay Phelps | @_jayphelps • Makes it easier to compose and control complex async tasks, over any amount of time • You don't need to manage your own Rx subscriptions • You can use redux tooling
  90. 90. But… Jay Phelps | @_jayphelps
  91. 91. Jay Phelps | @_jayphelps You should probably know redux and RxJS in advance
  92. 92. Jay Phelps | @_jayphelps RxJS has a bit of a learning curve
  93. 93. Jay Phelps | @_jayphelps “Reactive Programming”
  94. 94. Co-author Jay Phelps | @_jayphelps Ben Lesh Senior UI Engineer | @benlesh
  95. 95. Jay Phelps | @_jayphelps https://redux-observable.js.org
  96. 96. Thanks! @_jayphelps

×