Diese Präsentation wurde erfolgreich gemeldet.
Wir verwenden Ihre LinkedIn Profilangaben und Informationen zu Ihren Aktivitäten, um Anzeigen zu personalisieren und Ihnen relevantere Inhalte anzuzeigen. Sie können Ihre Anzeigeneinstellungen jederzeit ändern.

RxJS + Redux + React = Amazing

25.035 Aufrufe

Veröffentlicht am

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

Veröffentlicht in: Software
  • Als Erste(r) kommentieren

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

×