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

Hotwire with StimulusReflex

Hotwire with StimulusReflex

A brief introduction into both libraries, as well as my own subjective heuristics concerning when to use what.

Julian Rubisch

October 29, 2021
Tweet

Other Decks in Technology

Transcript

  1. Hi, I'm Julian and I'm on the StimulusReflex core team.

    ( ! ) Twitter: @julian_rubisch Github: @julianrubisch / @stimulusreflex Discord: https://discord.gg/stimulus-reflex 1
  2. Here are some of the questions I get asked most

    frequently: → "Should I use Hotwire or StimulusReflex?" 2
  3. Here are some of the questions I get asked most

    frequently: → "Should I use Hotwire or StimulusReflex?" → "Will Hotwire kill StimulusReflex?" 3
  4. Here are some of the questions I get asked most

    frequently: → "Should I use Hotwire or StimulusReflex?" → "Will Hotwire kill StimulusReflex?" → "Is Hotwire the StimulusReflex successor?" 4
  5. Here are some of the questions I get asked most

    frequently: → "Should I use Hotwire or StimulusReflex?" → "Will Hotwire kill StimulusReflex?" → "Is Hotwire the StimulusReflex successor?" → "Turbo to replace StimulusReflex?" 5
  6. Here are some of the questions I get asked most

    frequently: → "Should I use Hotwire or StimulusReflex?" → "Will Hotwire kill StimulusReflex?" → "Is Hotwire the StimulusReflex successor?" → "Turbo to replace StimulusReflex?" 6
  7. 7

  8. First, some definitions Hotwire is an "umbrella" brand of the

    new Basecamp/Rails frontend stack → Turbo → Stimulus → Strada (?) 8
  9. First, some definitions Turbo 1. Turbo Drive formerly known as

    Turbolinks, intercepts link navigation 2. Turbo Frames decomposed pages, scope navigation 3. Turbo Streams deliver page changes over WS, SSE, respond to form submissions 9
  10. First, some definitions Turbo Drive 1. Application Visits → advance

    (history.pushState) → replace (history.replaceState) 2. Restoration Visits → restores from Turbo cache (incl. scroll position) → ⬅ or ➡ browser buttons Opt out: data-turbo="false" 10
  11. First, some definitions Turbo Frames → Decompose the page into

    parts to be updated on request. → Links or forms are captured and automatically updated (regardless if full document or fragment) 11
  12. First, some definitions Turbo Frames <turbo-frame id="list"> <a href="/tracks/2" data-turbo-frame="track">

    </turbo-frame> <turbo-frame id="track"> <audio src="/tracks/1.mp3"></audio> </turbo-frame> 12
  13. First, some definitions Turbo Frames <turbo-frame id="list"> <a href="/tracks/2" data-turbo-frame="track">

    </turbo-frame> <turbo-frame id="track"> <audio src="/tracks/1.mp3"></audio> </turbo-frame> 13
  14. First, some definitions Turbo Frames <body> <h1>Track 2</h1> <turbo-frame id="track">

    <audio src="/tracks/2.mp3"></audio> </turbo-frame> </body> 14
  15. First, some definitions Turbo Frames <turbo-frame id="list"> <a href="/tracks/2" data-turbo-frame="track">

    </turbo-frame> <turbo-frame id="track"> <audio src="/tracks/2.mp3"></audio> </turbo-frame> 15
  16. First, some definitions Turbo Streams Turbo Streams deliver page changes

    as fragments of HTML wrapped in self-executing <turbo-stream> elements. → target ID + action, usually via WebSockets → targets can also be CSS selectors (.messages etc.) <turbo-stream action="replace" target="track"> <template> <div id="track"> <audio src="/tracks/1.mp3"> </div> </template> </turbo-stream> 16
  17. First, some definitions Turbo Streams Available Actions: → append (to

    a list) → prepend (before a list) → (insert) before → (insert) after → replace → update → remove 17
  18. First, some definitions Turbo Streams Responding to Form Submissions def

    destroy @track = Track.find(params[:id]) @track.destroy respond_to do |format| format.turbo_stream { render turbo_stream: turbo_stream.remove(@track) } end end 18
  19. First, some definitions StimulusReflex → a library exclusively for Reactive

    Rails, somewhat inspired by Phoenix LiveView → based on CableReady (the "missing ActionCable standard library"1) → initially by Nathan Hopkins (@hopsoft), now maintained by a 4- headed core team → Demo 1 Yours Truly 19
  20. More Qualifications 1. I'm biased ! 2. Still, I try

    to follow the Rails Golden Path as far as possible 3. In my own apps, I use < 10% Reflexes What follows is an assortment of juxtapositions, distilled into my (very subjective) Best Practices/Heuristics 20
  21. Paradigm Turbo → REST → Request ➡ Response → State

    Manipulation follows strict REST principles (form submits) StimulusReflex → RPC → Full-Duplex Persistent Websocket Connection → Any valid JS Event Emitter can initiate State Manipulation (<a>, <button>, <canvas>, custom elements/ web components, ...) 23
  22. Reactivity Turbo → 7 Turbo Stream operations → Lifecycle: →

    turbo:before-stream-render → turbo:frame-render → turbo:frame-load StimulusReflex → morphdom + 33 operations via CableReady → Lifecycle: → Server-Side before_, after_, around_reflex callbacks → Client-Side before, success, error, halted, (after), finalize callacks → Generic and Custom client-side lifecycle methods (e.g. <a href="#" data-reflex="click- >Example#poke">Poke</a> beforePoke(element) {}, etc.) 26
  23. Scope Turbo → Turbo Frames (referenced by IDs) ! →

    stateless, behaviorless quasi-"components" → Turbo Stream targets StimulusReflex → Page Morphs (optional data- reflex-root) → Selector Morphs (bypass conventional rendering completely, morph a single HTML element) → Nothing Morphs ( ) 27
  24. Refactoring Example - A Music Playlist 1. First Pass -

    Tabbed Navigation ! Turbo Frames! <% active = session["list_#{@list.id}"][:active] %> <%= turbo_frame_tag "list_entries" %> <a href="/tracks/1" data-turbo-frame="active_track"> ... <% end %> <%= turbo_frame_tag "active_track", src: track_url(active) %> 29
  25. Refactoring Example - A Music Playlist 2. Second Pass -

    Session Management class TracksController < ApplicationController def show @track = Track.find(params[:id]) session["list_#{@track.list.id}"][:active] = @track end end 30
  26. Refactoring Example - A Music Playlist 2. Second Pass -

    Session Management class TracksController < ApplicationController def show @track = Track.find(params[:id]) session["list_#{@track.list.id}"][:active] = @track end end ❌ This is a REST violation, we're mutating state on GET instead of POST. 31
  27. Refactoring Example - A Music Playlist 3. Third Pass -

    Hidden Form & Turbo Streams <%= form_with(model: @list, url: active_track_list_path(@list), id: dom_id(@list, :active_track)) do |f| %> <%= f.hidden_field :active_track_id, {value: active.id} %> <% end %> <%= turbo_frame_tag "active_track", src: track_url(active) if active.present? %> 32
  28. Refactoring Example - A Music Playlist 3. Third Pass -

    Hidden Form & Turbo Streams class ListsController < ApplicationController # PATCH def active_track session["list_#{@list.id}"][:active] = Track.find(list_params[:active_track_id]).list_entry.to_gid.to_s render turbo_stream: turbo_stream.replace(@list) end private def list_params params.require(:list).permit(:active_track_id) end end Problem: There's still (a lot of) flicker ⚡ 33
  29. Refactoring Example - A Music Playlist 4. Preferred Approach -

    Update Just a Data Attribute of a Stimulus Controller <turbo-frame id="list"> <div data-controller="list" data-list-active-value="<%= active.id ">...</div> </turbo-frame> <turbo-frame id="active_track"> <div data-controller="player" data-player-active-value="<%= active.id ">...</div> </turbo-frame> ❌ Currently not doable with streams ! Possible remedy: Stream a fragment containing a separate stimulus controller updating just the data attributes 34
  30. Refactoring Example - A Music Playlist 5. Here's an Equivalent

    Reflex <!-- view --> <% active = session["list_#{@list.id}"][:active] %> <a data-reflex="click->ActiveTrack#set_active_track" data-track-sgid="<%= track.to_sgid.to_s %>">Track 2</a> <!-- ... --> <audio src="<%= active.audio_url %>"></audio> # Reflex class ActiveTrackReflex < ApplicationReflex def set_active_track track = element.signed[:track_sgid] session["list_#{track.list}"][:active] = track end end 35
  31. Conclusions → Use Turbo to prepare application state, use SR

    to act on it → Use Turbo for everything that's covered by an HTTP verb (navigation, forms), else use SR → Docs! (DHH: The code == the docs ) 36
  32. Resources → David Colby's Blog → Just Enough Hotwire for

    Rails Developers → My Blog → SR Docs → CR Docs → Discord 37