Slide 1

Slide 1 text

Cameron Dutro, May 2024 Isomorphic View Components Re-imagining the Rails front-end

Slide 2

Slide 2 text

Who is this guy? Cameron Dutro GitHub, Inc @[email protected] github.com/camertron

Slide 3

Slide 3 text

Who is this guy? ViewComponent maintainer Primer maintainer primer/view_components primer/react primer/css

Slide 4

Slide 4 text

My journey to GitHub • Sep - Dec 2010: Fluther • 2011 - 2014: Twitter • 2014 - 2020: Lumos Labs • 2020 - 2021: Salesforce (Quip) • 2021 - now: GitHub Rails renaissance

Slide 5

Slide 5 text

Rails renaissance • Hotwire • Turbo, Turbo Native • RailsWorld announced • ViewComponent was having a moment

Slide 6

Slide 6 text

ViewComponent A framework for creating reusable, testable & encapsulated view components, built to integrate seamlessly with Ruby on Rails. “ ” viewcomponent.org

Slide 7

Slide 7 text

ViewComponent example # app/components/greeting_component.rb class GreetingComponent < ViewComponent::Base def initialize(people:) @people = people end end <%# app/components/greeting_component.html.erb %>
<% @people.each do |person| %> <%= render(NameComponent.new( first_name: person[:first_name]) last_name: person[:last_name] )) %> <% end %>

Slide 8

Slide 8 text

Rux # app/components/greeting_component.rux class GreetingComponent < ViewComponent::Base def initialize(people:) @people = people end def call
{@people.map do |person| end}
end end Star 385

Slide 9

Slide 9 text

The story continues • February 2024: I ported GitHub’s global user nav drawer to React • It’s a fantastic experience; React is awesome

Slide 10

Slide 10 text

The story continues • March 2024: Sin City Ruby conference in Las Vegas • Jason Charnes live-codes an interactive site builder using Rails front-end tooling • Company tried to use Stimulus, Hotwire, Turbo, etc, but ultimately wrote the feature in React • Painful watching Jason switch between so many fi les, connecting things with IDs, etc

Slide 11

Slide 11 text

ViewComponent interactivity • Render server-side • Use Web Components to attach dynamic behavior • Use github/catalyst to reduce Web Component boilerplate

Slide 12

Slide 12 text

Web Component example import {controller, target} from '@github/catalyst' @controller class PrimerTextFieldElement extends HTMLElement { @target inputElement: HTMLInputElement @target validationElement: HTMLElement @target validationErrorIcon: HTMLElement toggleValidationStyling(isError: boolean): void { if (isError) { this.validationElement.classList.remove('FormControl-inlineValidation--success') } else { this.validationElement.classList.add('FormControl-inlineValidation--success') } this.validationSuccessIcon.hidden = isError this.validationErrorIcon.hidden = !isError this.inputElement.setAttribute('invalid', isError ? 'true' : 'false') } setError(message: string): void { this.toggleValidationStyling(true) this.setValidationMessage(message) this.validationElement.hidden = false } }

Slide 13

Slide 13 text

Limitations of Web Components • Imperative, jquery-like code • Find elements, add/remove classes, hide/show, etc • Easy to make a mistake or miss an edge case • Lots of event listener gotchas • Hard to re-render since there’s no template • Di ffi cult to synchronize data model and view layer

Slide 14

Slide 14 text

React • Mature; large ecosystem • Declarative template, event handlers, refs, etc • Much less error-prone code, easier to reason about • Data binding (updating state updates the view) • Simply magical - no more querySelector().setAttribute() • Template (JSX) lives inside the component

Slide 15

Slide 15 text

After working in React again, I came to a harsh realization:

Slide 16

Slide 16 text

React is much, much better than Hotwire, Stimulus, or anything else the Rails front-end has to offer.

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

React Problems • Dynamism makes it more di ffi cult to write accessible components • No such thing as “static” content • Tends to take over the whole page • Large bundle sizes • Performance concerns on low-end devices • You don’t get to write Ruby

Slide 19

Slide 19 text

React competitors • Angular • Svelte • Vue • Solid • Qwik • Preact • Next/nuxt

Slide 20

Slide 20 text

Takeaways • Components are good • Reactivity is good • Declarative is better than imperative

Slide 21

Slide 21 text

Isomorphic view components What if we could render view components in the browser? “ ” me after Sin City Ruby

Slide 22

Slide 22 text

My dream class TodoComponent def call

{@name}

{@duration}

end end

Slide 23

Slide 23 text

My dream class TodoComponent def call

{@name}

{@duration}

{ set_state(edit: true) }}> Edit
end end

Slide 24

Slide 24 text

My dream class TodoComponent def call
{if @edit else

{@name}

{@duration}

{ set_state(edit: true) }}> Edit end}
end end

Slide 25

Slide 25 text

My dream class TodoComponent def call
{if @edit { save_changes }}> Save else

{@name}

{@duration}

{ set_state(edit: true) }}> Edit end}
end private def save_changes Net::HTTP.post_form(...) end end

Slide 26

Slide 26 text

View components in the browser • Need a Ruby runtime • Need some way to update the view when state changes • Need a way for Ruby to call JavaScript functions and wrap JavaScript objects

Slide 27

Slide 27 text

Ruby runtimes • ruby.wasm • MRI Ruby compiled to Web Assembly (WASM) • Large download (~50mb) • Opal.js • Required transpiler step • Sort of a large footprint last time I checked (~400k) + transpiled code

Slide 28

Slide 28 text

Prior art • Volt • Appears to be unmaintained • Hyperstack • Personally not a fan of the syntax • Built on top of React

Slide 29

Slide 29 text

Garnet.js • TypeScript implementation of YARV, the Ruby virtual machine • No transpilation - runs Ruby code directly • Smaller footprint (~95k) + Ruby code • Uses the new Prism parser

Slide 30

Slide 30 text

Garnet.js • Passes the test suites from several gems • Small but capable subset of the Ruby standard library • Decent performance

Slide 31

Slide 31 text

Neato • Experimental front-end framework for Rails • Uses Rux for templating • Compiles templates to tagged template literals • Uses Garnet to run Ruby code • Uses Lit for rendering and state management

Slide 32

Slide 32 text

Lit is a simple library for building fast, lightweight web components. At Lit's core is a … base class that provides reactive state, scoped styles, and a declarative template system that's tiny, fast and expressive. “ ” lit.dev/docs

Slide 33

Slide 33 text

• Lightweight: 5kb • Built on Web Components • Supports isolating CSS styles via browser-native shadow DOM • Uses tagged template literals for templating • “You can build just about any kind of web UI with Lit” - lit.dev

Slide 34

Slide 34 text

TodoListComponent class TodoListComponent include Neato::Component state :todos def initialize(todos:) @todos = todos end def call {@todos.map do |todo| end} end end

Slide 35

Slide 35 text

TodoComponent class TodoComponent include Neato::Component state :todo, :errors, :edit refs :name_input, :duration_input def call {if @edit <> {if @errors.include?(:name)

Name {@errors[:name].first}

end} > else @todo[:name] end} … end end

Slide 36

Slide 36 text

TodoComponent class TodoComponent def call ... {if @edit { save_changes }}> Save else { set_state(edit: true) }}> Edit end} ... end end

Slide 37

Slide 37 text

TodoComponent class TodoComponent private def save_changes response = Neato.fetch("/todos/#{@todo[:id]}", { method: 'PUT', headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: Neato::URLSearchParams.new( "todo[name]": name_input.value, "todo[duration_mins]": duration_input.value ) }) if response.ok? set_state(todo: response.json[:todo], edit: false, errors: {}) else @todo[:name] = name_input.value @todo[:duration_mins] = duration_input.value set_state( errors: response.json[:errors] ) end end end

Slide 38

Slide 38 text

Demo time!