Building UI Components

Building UI Components

Ruby2JS provides multiple approaches for building frontend UI components. This guide explains the options, their trade-offs, and when to use each.

The Vision: Portable Components

Ruby2JS enables a “write once, target both” approach to component development. The same Ruby code can produce different JavaScript outputs depending on your needs:

                    ┌─────────────────────────────────────┐
                    │    Phlex Ruby (your component)      │
                    │  div { h1 { @title } }              │
                    └─────────────┬───────────────────────┘
                                  │
                    ┌─────────────┴───────────────────────┐
                    │                                     │
                    ↓                                     ↓
             [:phlex] filter                    [:phlex, :react] filters
                    │                                     │
                    ↓                                     ↓
            ┌──────────────┐                    ┌──────────────┐
            │  Phlex JS    │                    │  React JS    │
            │ (HTML strings│                    │ (virtual DOM)│
            │  + Stimulus) │                    │              │
            └──────────────┘                    └──────────────┘

Why this matters:

  • Migration flexibility — Start with React, switch to lighter Phlex when reactivity isn’t needed
  • Server/client parity — Same Phlex component works on server (Ruby) and browser (transpiled JS)
  • Framework choice deferred — Write components now, choose runtime later
  • Gradual adoption — Mix approaches within the same application

Approaches Compared

1. React with JSX Syntax

For full React applications with complex state management:

class Counter < React
  def initialize
    @count = 0
  end

  def render
    %x{
      <div>
        <p>Count: {count}</p>
        <button onClick={() => setCount(count + 1)}>+</button>
      </div>
    }
  end
end

Best for:

  • Complex state management
  • Applications already using React
  • When you need React’s ecosystem (hooks, context, libraries)

Trade-offs:

  • Requires React runtime (~40KB min+gzip)
  • JSX syntax differs from Ruby idioms
  • Virtual DOM overhead for simple UIs

2. Phlex → Phlex JS (Lightweight)

For server-rendered HTML with lightweight client-side interactivity:

class Card < Phlex::HTML
  def initialize(title:)
    @title = title
  end

  def view_template
    div(class: "card") do
      h1 { @title }
      p { "Card content" }
    end
  end
end

Best for:

  • Server-rendered applications (Rails, Sinatra)
  • Static or mostly-static UIs
  • When paired with Stimulus for interactivity
  • Minimizing JavaScript bundle size

Trade-offs:

  • No virtual DOM or automatic re-rendering
  • Requires Stimulus or similar for interactivity
  • String concatenation (less efficient for frequent updates)

3. Phlex → React JS (Portable)

Write Phlex, output React — the best of both worlds:

class Card < Phlex::HTML
  def initialize(title:)
    @title = title
  end

  def view_template
    div(class: "card") do
      h1 { @title }
      p { "Card content" }
    end
  end
end

Best for:

  • Teams familiar with Phlex who need React output
  • Migrating from React to Phlex (or vice versa)
  • Sharing components between server and client
  • Keeping options open

Trade-offs:

  • Phlex DSL doesn’t expose all React features directly
  • Two mental models to understand

When to Use What

Scenario Recommended Approach
New React app React with JSX
Rails app, minimal JS Phlex + Stimulus
Rails app, complex UI sections Phlex → React for those sections
Migrating away from React Phlex → React now, Phlex later
Maximum portability Phlex (can target either)
Need React hooks/context React with JSX
Static marketing pages Phlex + Stimulus
Interactive dashboards React or Phlex → React

Phlex + Stimulus: A React Alternative

For many applications, the combination of Phlex (for HTML) and Stimulus (for behavior) provides a lighter alternative to React:

# Phlex component with Stimulus hooks
class Counter < Phlex::HTML
  def view_template
    div(data_controller: "counter") do
      span(data_counter_target: "display") { "0" }
      button(data_action: "click->counter#increment") { "+" }
    end
  end
end

# Stimulus controller
class CounterController < Stimulus::Controller
  def connect
    @count = 0
  end

  def increment
    @count += 1
    displayTarget.textContent = @count.to_s
  end
end
Concern React Phlex + Stimulus
Initial HTML Virtual DOM render Server or Phlex JS
State useState/useReducer Controller instance
Updates Re-render → diff → patch Direct DOM manipulation
Bundle size ~40KB+ ~3KB (Stimulus)
Mental model Declarative Imperative

Choose Phlex + Stimulus when:

  • Updates are infrequent or localized
  • You want HTML-first development
  • Bundle size matters
  • You’re already using Rails/Hotwire

Choose React when:

  • UI has complex, frequent state changes
  • You need component composition with shared state
  • You want the React ecosystem

The Architecture: How It Works

Ruby2JS uses pnodes (Phlex nodes) as a unified intermediate representation:

# Your Phlex code
div(class: "card") { h1 { @title } }

# Becomes a pnode (internal AST)
s(:pnode, :div, s(:hash, s(:pair, s(:sym, :class), s(:str, "card"))),
  s(:pnode, :h1, s(:hash),
    s(:pnode_text, s(:lvar, :title))))

This pnode can then be converted to:

  • Template literals (Phlex JS): `<div class="card"><h1>${title}</h1></div>`
  • React.createElement (React JS): React.createElement("div", {className: "card"}, ...)

The filter chain determines the output:

# Phlex JS output
Ruby2JS.convert(code, filters: [:phlex])

# React JS output
Ruby2JS.convert(code, filters: [:phlex, :react])

Examples

Same Component, Different Outputs

Source (Phlex Ruby):

class ProfileCard < Phlex::HTML
  def initialize(name:, avatar:)
    @name = name
    @avatar = avatar
  end

  def view_template
    div(class: "profile") do
      img(src: @avatar, alt: @name)
      h2 { @name }
    end
  end
end

With [:phlex]:

class ProfileCard extends Phlex.HTML {
  render({avatar, name}) {
    let _phlex_out = "";
    _phlex_out += `<div class="profile">` +
      `<img src="${avatar}" alt="${name}">` +
      `<h2>${String(name)}</h2></div>`;
    return _phlex_out
  }
}

With [:phlex, :react]:

class ProfileCard extends Phlex.HTML {
  render({avatar, name}) {
    return React.createElement("div", {className: "profile"},
      React.createElement("img", {src: avatar, alt: name}),
      React.createElement("h2", null, name)
    )
  }
}

Component with Multiple Root Elements

When a component has multiple root elements, React mode automatically wraps them in a Fragment:

class PageHeader < Phlex::HTML
  def view_template
    h1 { "Welcome" }
    nav { a(href: "/") { "Home" } }
  end
end
// With [:phlex, :react]
class PageHeader extends Phlex.HTML {
  render() {
    return React.createElement(React.Fragment, null,
      React.createElement("h1", null, "Welcome"),
      React.createElement("nav", null,
        React.createElement("a", {href: "/"}, "Home")
      )
    )
  }
}

Getting Started

  1. For React apps: Use the React filter with JSX syntax
  2. For Rails/server-rendered apps: Use the Phlex filter with Stimulus filter
  3. For maximum flexibility: Write Phlex components and choose your output target

See the filter documentation for detailed API reference: