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

GroupBy: RxJS’ Secret Weapon for Complex UIs

7beed3a6fa39e12c9e873b903e4d9244?s=47 Sam Julien
December 01, 2020

GroupBy: RxJS’ Secret Weapon for Complex UIs

What if we told you that one of the most useful operators in RxJS is one you probably have never heard of? In this talk, you'll meet the quiet workhorse called "groupBy." Don't let its understated nature fool you -- groupBy can change your life! The groupBy operator thrives in heavily interactive UIs, like when users rapidly favorite a bunch of items in a list. When combined with some of the other operators like concatMap, groupBy really shines. In this talk, you'll learn how to harness its power and take your reactive code to the next level. You will see how to use groupBy to give users an excellent experience, handling race conditions and back pressure with ease. You will also learn the *why* behind when to use groupBy and common pitfalls to avoid with it. Of course, you'll also leave with thorough sample code you can take back to work and start building from right away.

7beed3a6fa39e12c9e873b903e4d9244?s=128

Sam Julien

December 01, 2020
Tweet

Transcript

  1. None
  2. RXJS'S SECRET WEAPON Mike Ryan | Sam Julien

  3. @mikeryandev @samjulien COMPLEX UI @mikeryandev @samjulien

  4. @mikeryandev @samjulien HIGHLY INTERACTIVE UI @mikeryandev @samjulien

  5. @mikeryandev @samjulien @mikeryandev @samjulien

  6. @mikeryandev @samjulien @mikeryandev @samjulien

  7. @mikeryandev @samjulien @mikeryandev @samjulien

  8. @mikeryandev @samjulien @mikeryandev @samjulien

  9. @mikeryandev @samjulien @mikeryandev @samjulien

  10. @mikeryandev @samjulien MIKE RYAN @mikeryandev

  11. @mikeryandev @samjulien SOFTWARE ARCHITECT AT SYNAPSE MIKE RYAN @mikeryandev

  12. @mikeryandev @samjulien SOFTWARE ARCHITECT AT SYNAPSE GOOGLE DEVELOPER EXPERT MIKE

    RYAN @mikeryandev
  13. @mikeryandev @samjulien SOFTWARE ARCHITECT AT SYNAPSE GOOGLE DEVELOPER EXPERT NGRX

    CORE TEAM MIKE RYAN @mikeryandev
  14. @mikeryandev @samjulien SAM JULIEN @samjulien

  15. @mikeryandev @samjulien SAM JULIEN @samjulien DEVREL AT AUTH0

  16. @mikeryandev @samjulien SAM JULIEN @samjulien DEVREL AT AUTH0 GDE &

    ANGULAR COLLABORATOR
  17. @mikeryandev @samjulien SAM JULIEN @samjulien DEVREL AT AUTH0 GDE &

    ANGULAR COLLABORATOR SAMJULIEN.COM
  18. @mikeryandev @samjulien WARNING THIS IS ADVANCED @mikeryandev @samjulien

  19. @mikeryandev @samjulien

  20. @mikeryandev @samjulien

  21. @mikeryandev @samjulien

  22. @mikeryandev @samjulien

  23. @mikeryandev @samjulien @mikeryandev @samjulien

  24. @mikeryandev @samjulien @mikeryandev @samjulien

  25. @mikeryandev @samjulien @mikeryandev @samjulien

  26. @mikeryandev @samjulien DISPATCHER PATTERN

  27. @mikeryandev @samjulien UI COMPONENTS

  28. @mikeryandev @samjulien UI COMPONENTS ACTIONS

  29. @mikeryandev @samjulien UI COMPONENTS DISPATCHER ACTIONS

  30. @mikeryandev @samjulien UI COMPONENTS DISPATCHER ACTIONS API

  31. @mikeryandev @samjulien UI COMPONENTS DISPATCHER ACTIONS API

  32. @mikeryandev @samjulien

  33. @mikeryandev @samjulien const dispatcher = new Subject<Movie>(); @mikeryandev @samjulien

  34. @mikeryandev @samjulien const movie1$: Observable<Event> = fromEvent(button1, ‘click'); movie1$.subscribe(() =>

    dispatcher.next({ movieId: 1 })); @mikeryandev @samjulien
  35. @mikeryandev @samjulien const movie1$: Observable<Event> = fromEvent(button1, ‘click'); movie1$.subscribe(() =>

    dispatcher.next({ movieId: 1 })); @mikeryandev @samjulien
  36. @mikeryandev @samjulien const movie1$: Observable<Event> = fromEvent(button1, ‘click'); movie1$.subscribe(() =>

    dispatcher.next({ movieId: 1 })); @mikeryandev @samjulien
  37. @mikeryandev @samjulien HOW DO WE MAP THE BUTTON CLICKS TO

    THE ENDPOINT CALLS? @mikeryandev @samjulien
  38. @mikeryandev @samjulien const actions$ = dispatcher.asObservable().pipe( tap(({ movieId }) =>

    setButtonEmoji(movieId)), ???(movie => toggleStatus(movie.movieId)) ); @mikeryandev @samjulien
  39. @mikeryandev @samjulien const actions$ = dispatcher.asObservable().pipe( tap(({ movieId }) =>

    setButtonEmoji(movieId)), ???(movie => toggleStatus(movie.movieId)) ); @mikeryandev @samjulien
  40. @mikeryandev @samjulien const actions$ = dispatcher.asObservable().pipe( tap(({ movieId }) =>

    setButtonEmoji(movieId)), ???(movie => toggleStatus(movie.movieId)) ); @mikeryandev @samjulien
  41. @mikeryandev @samjulien const actions$ = dispatcher.asObservable().pipe( tap(({ movieId }) =>

    setButtonEmoji(movieId)), ???(movie => toggleStatus(movie.movieId)) ); @mikeryandev @samjulien
  42. @mikeryandev @samjulien MAPPING OPERATORS @mikeryandev @samjulien

  43. @mikeryandev @samjulien SORTING PACKAGES @mikeryandev @samjulien

  44. @mikeryandev @samjulien @mikeryandev @samjulien

  45. @mikeryandev @samjulien

  46. @mikeryandev @samjulien

  47. @mikeryandev @samjulien MERGEMAP @mikeryandev @samjulien

  48. @mikeryandev @samjulien mergeMap

  49. @mikeryandev @samjulien mergeMap

  50. @mikeryandev @samjulien const actions$ = dispatcher.asObservable().pipe( tap(({ movieId }) =>

    setButtonEmoji(movieId)), mergeMap(movie => toggleStatus(movie.movieId)) ); @mikeryandev @samjulien
  51. @mikeryandev @samjulien @mikeryandev @samjulien

  52. @mikeryandev @samjulien @mikeryandev @samjulien

  53. @mikeryandev @samjulien WHAT HAPPENS WHEN WE CLICK TOO FAST?

  54. @mikeryandev @samjulien @mikeryandev @samjulien

  55. @mikeryandev @samjulien @mikeryandev @samjulien

  56. @mikeryandev @samjulien RACE CONDITIONS @mikeryandev @samjulien

  57. @mikeryandev @samjulien CONCATMAP @mikeryandev @samjulien

  58. @mikeryandev @samjulien concatMap

  59. @mikeryandev @samjulien concatMap

  60. @mikeryandev @samjulien @mikeryandev @samjulien const actions$ = dispatcher.asObservable().pipe( tap(({ movieId

    }) => setButtonEmoji(movieId)), concatMap(movie => toggleStatus(movie.movieId)) );
  61. @mikeryandev @samjulien @mikeryandev @samjulien

  62. @mikeryandev @samjulien @mikeryandev @samjulien

  63. @mikeryandev @samjulien BACK PRESSURE @mikeryandev @samjulien

  64. @mikeryandev @samjulien REQUESTS > BUFFER SIZE @mikeryandev @samjulien

  65. @mikeryandev @samjulien @mikeryandev @samjulien

  66. @mikeryandev @samjulien @mikeryandev @samjulien

  67. @mikeryandev @samjulien SWITCHMAP @mikeryandev @samjulien

  68. @mikeryandev @samjulien switchMap

  69. @mikeryandev @samjulien switchMap

  70. @mikeryandev @samjulien @mikeryandev @samjulien const actions$ = dispatcher.asObservable().pipe( tap(({ movieId

    }) => setButtonEmoji(movieId)), switchMap(movie => toggleStatus(movie.movieId)) );
  71. @mikeryandev @samjulien @mikeryandev @samjulien

  72. @mikeryandev @samjulien @mikeryandev @samjulien

  73. @mikeryandev @samjulien WAIT A SECOND… @mikeryandev @samjulien

  74. @mikeryandev @samjulien @mikeryandev @samjulien

  75. @mikeryandev @samjulien @mikeryandev @samjulien

  76. @mikeryandev @samjulien @mikeryandev @samjulien

  77. @mikeryandev @samjulien @mikeryandev @samjulien

  78. @mikeryandev @samjulien STRANGELY, THIS MAKES SENSE… @mikeryandev @samjulien

  79. @mikeryandev @samjulien @mikeryandev @samjulien const actions$ = dispatcher.asObservable().pipe( tap(({ movieId

    }) => setButtonEmoji(movieId)), switchMap(movie => toggleStatus(movie.movieId)) );
  80. @mikeryandev @samjulien @mikeryandev @samjulien const actions$ = dispatcher.asObservable().pipe( tap(({ movieId

    }) => setButtonEmoji(movieId)), switchMap(movie => toggleStatus(movie.movieId)) );
  81. @mikeryandev @samjulien GROUPBY @mikeryandev @samjulien

  82. @mikeryandev @samjulien concatMap groupBy

  83. @mikeryandev @samjulien concatMap groupBy

  84. @mikeryandev @samjulien concatMap groupBy

  85. @mikeryandev @samjulien concatMap groupBy

  86. @mikeryandev @samjulien switchMap groupBy

  87. @mikeryandev @samjulien switchMap groupBy

  88. @mikeryandev @samjulien switchMap groupBy

  89. @mikeryandev @samjulien switchMap groupBy

  90. @mikeryandev @samjulien @mikeryandev @samjulien

  91. @mikeryandev @samjulien @mikeryandev @samjulien

  92. @mikeryandev @samjulien WHAT ABOUT BOTH AT ONCE? @mikeryandev @samjulien

  93. @mikeryandev @samjulien @mikeryandev @samjulien

  94. @mikeryandev @samjulien @mikeryandev @samjulien

  95. @mikeryandev @samjulien HOW DO WE DO IT? @mikeryandev @samjulien

  96. @mikeryandev @samjulien HIGHER ORDER OBSERVABLES @mikeryandev @samjulien

  97. [, , ] @mikeryandev @samjulien

  98. [[, , ], [, , ], [, , ]] @mikeryandev

    @samjulien
  99. [[, , ], [, , ], [, , ]] array.flat()

    [, , , , , , , , ] @mikeryandev @samjulien
  100. of(, , ) @mikeryandev @samjulien

  101. of(of(, , ), of(, , ), of(, , )) @mikeryandev

    @samjulien
  102. of(of(, , ), of(, , ), of(, , )) observable$.pipe(mergeAll())

    of(, , , , , , , , ) @mikeryandev @samjulien
  103. observable$.pipe( map(movie => toggleStatus(movie)), mergeAll() ); observable$.pipe( mergeMap(movie => toggleStatus(movie))

    ); @mikeryandev @samjulien
  104. GROUPBY THE SECRET WEAPON @mikeryandev @samjulien

  105. packages$.pipe(groupBy(package => package.color)); COMPLETE COMPLETE COMPLETE COMPLETE @mikeryandev @samjulien

  106. packages$.pipe( groupBy(package => package.color), mergeAll() ); COMPLETE COMPLETE @mikeryandev @samjulien

  107. packages$.pipe( groupBy(package => package.color), mergeAll() ); @mikeryandev @samjulien

  108. actions$.pipe( groupBy(action => action.movieId), mergeAll() ); @mikeryandev @samjulien

  109. actions$.pipe( groupBy(action => action.movieId), map(actionsByMovieId$ => { return actionsByMovieId$.pipe( switchMap(action

    => { return toggleStatus(action.movieId); }) ); }), mergeAll() ); @mikeryandev @samjulien
  110. actions$.pipe( groupBy(action => action.movieId), map(actionsByMovieId$ => { return actionsByMovieId$.pipe( switchMap(action

    => { return toggleStatus(action.movieId); }) ); }), mergeAll() ); @mikeryandev @samjulien
  111. actions$.pipe( groupBy(action => action.movieId), map(actionsByMovieId$ => { return actionsByMovieId$.pipe( switchMap(action

    => { return toggleStatus(action.movieId); }) ); }), mergeAll() ); @mikeryandev @samjulien
  112. actions$.pipe( groupBy(action => action.movieId), map(actionsByMovieId$ => { return actionsByMovieId$.pipe( switchMap(action

    => { return toggleStatus(action.movieId); }) ); }), mergeAll() ); @mikeryandev @samjulien
  113. actions$.pipe( groupBy(action => action.movieId), map(actionsByMovieId$ => { return actionsByMovieId$.pipe( switchMap(action

    => { return toggleStatus(action.movieId); }) ); }), mergeAll() ); @mikeryandev @samjulien
  114. @mikeryandev @samjulien DEMO @mikeryandev @samjulien

  115. @mikeryandev @samjulien @mikeryandev @samjulien

  116. @mikeryandev @samjulien @mikeryandev @samjulien

  117. @mikeryandev @samjulien @mikeryandev @samjulien

  118. @mikeryandev @samjulien @mikeryandev @samjulien

  119. @mikeryandev @samjulien WE HAVE A PROBLEM… @mikeryandev @samjulien

  120. @mikeryandev @samjulien

  121. None
  122. None
  123. None
  124. None
  125. None
  126. None
  127. None
  128. None
  129. @mikeryandev @samjulien WE HAVE BUILT A VERY EFFICIENT MEMORY LEAK

    @mikeryandev @samjulien
  130. groupBy( /* Key Selector */, /* Element Selector */, /*

    Duration Selector */, ) @mikeryandev @samjulien
  131. @mikeryandev @samjulien Returns an observable that will clean up the

    group when it noti f i es or completes. DURATION SELECTOR @mikeryandev @samjulien
  132. groupBy( /* Key Selector */, /* Element Selector */, /*

    Duration Selector */, ) @mikeryandev @samjulien
  133. groupBy( action => action.movieId, action => action, group$ => /*

    discard group after 15s of silence */, ) @mikeryandev @samjulien
  134. @mikeryandev @samjulien HOW DO WE GET TO 15 SECONDS OF

    SILENCE? @mikeryandev @samjulien
  135. @mikeryandev @samjulien timeoutWith( /** Time in MS */, /** Observable

    to Switch To */ ); @mikeryandev @samjulien
  136. observable$.pipe(timeoutWith(15000, EMPTY)); COMPLETE COMPLETE @mikeryandev @samjulien

  137. @mikeryandev @samjulien Returns an observable that will clean up the

    group when it noti f i es or completes. DURATION SELECTOR @mikeryandev @samjulien
  138. @mikeryandev @samjulien Returns an observable that will clean up the

    group when it noti f i es or completes. DURATION SELECTOR @mikeryandev @samjulien
  139. observable$.pipe(timeoutWith(15000, EMPTY)); COMPLETE COMPLETE @mikeryandev @samjulien

  140. observable$.pipe(timeoutWith(15000, EMPTY)); COMPLETE COMPLETE Noti f i cation Completion @mikeryandev

    @samjulien
  141. @mikeryandev @samjulien DEMO @mikeryandev @samjulien

  142. @mikeryandev @samjulien

  143. @mikeryandev @samjulien

  144. @mikeryandev @samjulien WE HAVE REBUILT MERGEMAP @mikeryandev @samjulien

  145. observable$.pipe(???); @mikeryandev @samjulien COMPLETE COMPLETE

  146. @mikeryandev @samjulien HOW DO WE GET RID OF THE NOTIFICATIONS?

    @mikeryandev @samjulien
  147. observable$.pipe(ignoreElements()); COMPLETE COMPLETE @mikeryandev @samjulien

  148. observable$.pipe( timeoutWith(15000, EMPTY), ignoreElements() ); @mikeryandev @samjulien

  149. observable$.pipe( timeoutWith(15000, EMPTY), ignoreElements() ); @mikeryandev @samjulien

  150. observable$.pipe( timeoutWith(15000, EMPTY), ignoreElements() ); @mikeryandev @samjulien

  151. observable$.pipe( timeoutWith(15000, EMPTY), ignoreElements() ); COMPLETE COMPLETE @mikeryandev @samjulien

  152. actions$.pipe( groupBy( action => action.movieId, action => action, actionsByMovieId$ =>

    actionsByMovieId$.pipe( timeoutWith(15000, EMPTY), ignoreElements() ) ) ); @mikeryandev @samjulien
  153. actions$.pipe( groupBy( action => action.movieId, action => action, actionsByMovieId$ =>

    actionsByMovieId$.pipe( timeoutWith(15000, EMPTY), ignoreElements() ) ) ); @mikeryandev @samjulien
  154. actions$.pipe( groupBy( action => action.movieId, action => action, actionsByMovieId$ =>

    actionsByMovieId$.pipe( timeoutWith(15000, EMPTY), ignoreElements() ) ) ); @mikeryandev @samjulien
  155. actions$.pipe( groupBy( action => action.movieId, action => action, actionsByMovieId$ =>

    actionsByMovieId$.pipe( timeoutWith(15000, EMPTY), ignoreElements() ) ) ); @mikeryandev @samjulien
  156. actions$.pipe( groupBy( action => action.movieId, action => action, actionsByMovieId$ =>

    actionsByMovieId$.pipe( timeoutWith(15000, EMPTY), ignoreElements() ) ) ); @mikeryandev @samjulien
  157. @mikeryandev @samjulien NO MORE HIGHLY EFFICIENT MEMORY LEAKS @mikeryandev @samjulien

  158. actions$.pipe( switchMap(action => toggleStatus(action.movieId)) ); @mikeryandev @samjulien

  159. actions$.pipe( groupBy( action => action.movieId, action => action, actionsByMovieId$ =>

    actionsByMovieId$.pipe( timeoutWith(15000, EMPTY), ignoreElements() ) ), map(actionsByMovieId$ => actionsByMovieId$.pipe( switchMap(action => toggleStatus(action.movieId)) ) ), mergeAll() );
  160. actions$.pipe( groupBy( action => action.movieId, action => action, actionsByMovieId$ =>

    actionsByMovieId$.pipe( timeoutWith(15000, EMPTY), ignoreElements() ) ), map(actionsByMovieId$ => actionsByMovieId$.pipe(
  161. actions$.pipe( groupBy( action => action.movieId, action => action, actionsByMovieId$ =>

    actionsByMovieId$.pipe( timeoutWith(15000, EMPTY), ignoreElements() ) ), map(actionsByMovieId$ => actionsByMovieId$.pipe(
  162. actionsByMovieId$.pipe( timeoutWith(15000, EMPTY), ignoreElements() ) ), map(actionsByMovieId$ => actionsByMovieId$.pipe( switchMap(action

    => toggleStatus(action.movieId)) ) ), mergeAll() );
  163. actionsByMovieId$.pipe( timeoutWith(15000, EMPTY), ignoreElements() ) ), map(actionsByMovieId$ => actionsByMovieId$.pipe( switchMap(action

    => toggleStatus(action.movieId)) ) ), mergeAll() );
  164. ) ), map(actionsByMovieId$ => actionsByMovieId$.pipe( switchMap(action => toggleStatus(action.movieId)) ) ),

    mergeAll() );
  165. ) ), map(actionsByMovieId$ => actionsByMovieId$.pipe( switchMap(action => toggleStatus(action.movieId)) ) ),

    mergeAll() );
  166. actions$.pipe( groupBy( action => action.movieId, action => action, actionsByMovieId$ =>

    actionsByMovieId$.pipe( timeoutWith(15000, EMPTY), ignoreElements() ) ), map(actionsByMovieId$ => actionsByMovieId$.pipe( switchMap(action => toggleStatus(action.movieId)) ) ), mergeAll() );
  167. actions$.pipe( groupBy( action => action.movieId, action => action, actionsByMovieId$ =>

    actionsByMovieId$.pipe( timeoutWith(15000, EMPTY), ignoreElements() ) ), map(actionsByMovieId$ => actionsByMovieId$.pipe( switchMap(action => toggleStatus(action.movieId)) ) ), mergeAll() ); Group By Movie ID
  168. actions$.pipe( groupBy( action => action.movieId, action => action, actionsByMovieId$ =>

    actionsByMovieId$.pipe( timeoutWith(15000, EMPTY), ignoreElements() ) ), map(actionsByMovieId$ => actionsByMovieId$.pipe( switchMap(action => toggleStatus(action.movieId)) ) ), mergeAll() ); Save Status
  169. actions$.pipe( groupBy( action => action.movieId, action => action, actionsByMovieId$ =>

    actionsByMovieId$.pipe( timeoutWith(15000, EMPTY), ignoreElements() ) ), map(actionsByMovieId$ => actionsByMovieId$.pipe( switchMap(action => toggleStatus(action.movieId)) ) ), mergeAll() ); Flatten Everything
  170. actions$.pipe( groupBy( action => action.movieId, action => action, actionsByMovieId$ =>

    actionsByMovieId$.pipe( timeoutWith(15000, EMPTY), ignoreElements() ) ), map(actionsByMovieId$ => actionsByMovieId$.pipe( switchMap(action => toggleStatus(action.movieId)) ) ), mergeAll() );
  171. actions$.pipe( groupBy( action => action.movieId, action => action, actionsByMovieId$ =>

    actionsByMovieId$.pipe( timeoutWith(15000, EMPTY), ignoreElements() ) ), mergeMap(actionsByMovieId$ => actionsByMovieId$.pipe( switchMap(action => toggleStatus(action.movieId)) ) ) );
  172. actions$.pipe( groupBy( action => action.movieId, action => action, actionsByMovieId$ =>

    actionsByMovieId$.pipe( timeoutWith(15000, EMPTY), ignoreElements() ) ), mergeMap(actionsByMovieId$ => actionsByMovieId$.pipe( switchMap(action => toggleStatus(action.movieId)) ) ) );
  173. @mikeryandev @samjulien DEMO @mikeryandev @samjulien

  174. @mikeryandev @samjulien @mikeryandev @samjulien

  175. @mikeryandev @samjulien @mikeryandev @samjulien

  176. @mikeryandev @samjulien @mikeryandev @samjulien

  177. @mikeryandev @samjulien @mikeryandev @samjulien

  178. @mikeryandev @samjulien UI COMPONENTS DISPATCHER ACTIONS API

  179. @mikeryandev @samjulien UI COMPONENTS DISPATCHER ACTIONS API POTENTIAL PERFORMANCE BOTTLENECKS

  180. @mikeryandev @samjulien UI COMPONENTS DISPATCHER ACTIONS API API API

  181. @mikeryandev @samjulien CUSTOM OPERATOR…? @mikeryandev @samjulien

  182. @mikeryandev @samjulien samj.im/groupby-talk @mikeryandev @samjulien

  183. @mikeryandev @samjulien samj.im/groupby-talk @mikeryandev @samjulien THANK YOU!