Patterns

Dual-Target Patterns

This guide covers patterns that work well when writing Ruby code that will run both natively and as transpiled JavaScript.

Table of Contents

Classes and Methods

Basic Classes

Classes translate naturally:

class Greeter
  def initialize(name)
    @name = name
  end

  def greet
    "Hello, #{@name}!"
  end
end

Note: With ES2022, instance variables become private fields (#name). With older ES levels or underscored_private: true, they use underscore prefix (_name).

Private Methods

Mark private methods and they’ll be prefixed appropriately in JavaScript. With ES2022, private methods use the # prefix (true JavaScript private methods). With older ES levels or underscored_private: true, they use _ prefix:

class Calculator
  def calculate(x)
    validate(x)
    process(x)
  end

  private

  def validate(x)
    raise "Invalid" unless x > 0
  end

  def process(x)
    x * 2
  end
end

Calls to private methods (with or without explicit self) are automatically prefixed to match the method definition.

Method Calls vs Property Access

Ruby2JS distinguishes between method calls and property access based on parentheses:

# No parens, no args - becomes property access
len = obj.length
first = arr.first

# Empty parens - becomes method call
result = obj.process()

# Parens with args - always method call
obj.set(42)

Data Structures

Arrays

Most array operations translate directly:

arr = [1, 2, 3]
arr.push(4)
arr.length
doubled = arr.map { |x| x * 2 }
big = arr.select { |x| x > 1 }
found = arr.find { |x| x > 1 }

Watch out: The << operator needs disambiguation:

items = [1, 2, 3]

# Solution 1: Use push explicitly
items.push(4)

# Solution 2: Use pragma for one-off cases
items << 5 # Pragma: array

Hashes/Objects

Ruby hashes become JavaScript objects:

options = { name: "test", count: 42 }
name = options[:name]
keys = options.keys()
values = options.values()

Rich Object Literals

JavaScript objects can have getters, setters, and methods—richer than Ruby hashes. Ruby2JS supports this via anonymous classes:

obj = Class.new do
  def initialize
    @count = 0
  end

  def increment
    @count += 1
  end

  def count
    @count
  end
end.new

This pattern is useful when you need a one-off object with behavior, not just data.

Hash Iteration

Ruby’s .each on hashes needs special handling:

hash = { a: 1, b: 2, c: 3 }

# Use the entries pragma for key-value iteration
hash.each { |k, v| console.log(k, v) } # Pragma: entries

Control Flow

Conditionals

Standard conditionals work as expected:

def classify(x)
  if x > 10
    "large"
  elsif x > 5
    "medium"
  else
    "small"
  end
end

Unless

def process(item)
  return if item.nil?

  unless item.empty?
    handle(item)
  end
end

Blocks and Lambdas

Blocks

Blocks become arrow functions by default. Try editing the code below:

items = [1, 2, 3, 4, 5]

doubled = items.map { |x| x * 2 }
evens = items.select { |x| x % 2 == 0 }
sum = items.reduce(0) { |acc, x| acc + x }

When You Need this

Arrow functions capture this lexically. For DOM event handlers where you need dynamic this:

# Arrow function - this is lexical (outer scope)
element.on("click") { handle(this) }

# Traditional function - this is the element
element.on("click") { handle(this) } # Pragma: noes2015

Lambdas

Lambdas with stabby syntax work well:

double = ->(x) { x * 2 }
result = double.call(21)

add = ->(a, b) { a + b }
sum = add.call(1, 2)

Variable Declarations

Local Variables

Ruby2JS tracks variable declarations and emits let appropriately:

x = 1       # First use: let x = 1
x = 2       # Reassignment: x = 2
y = x + 1   # let y = x + 1

Constants

MAX_SIZE = 100
PI = 3.14159

Working with First-Class Functions

When you have functions stored in variables:

handlers = {
  click: ->(e) { console.log("clicked", e) },
  hover: ->(e) { console.log("hovered", e) }
}

handler = handlers[:click]
handler.call(event) # Pragma: method

String Operations

Interpolation

String interpolation translates to template literals:

name = "World"
greeting = "Hello, #{name}!"
multi = "Count: #{1 + 2 + 3}"

Common Methods

str = "Hello World"

len = str.length
upper = str.upcase
lower = str.downcase
has_o = str.include?("o")
starts = str.start_with?("Hello")
ends = str.end_with?("World")
replaced = str.gsub(/o/, "0")

Module Organization

Exports

Using the ESM filter:

export class MyClass
  def process(x)
    x * 2
  end
end

export def helper_method(x)
  x + 1
end

export DEFAULT_VALUE = 42

Imports

import React, from: 'react'
import [useState, useEffect], from: 'react'
import MyModule, from: './my_module'

Ruby-Only Code

Skipping Code

Use # Pragma: skip to exclude Ruby-only code from JS output:

require 'json' # Pragma: skip

def ruby_only_helper # Pragma: skip
  # This method won't appear in JS
end

def works_in_both
  "Hello from Ruby or JS!"
end

For dual-target code, we recommend:

# ruby2js: preset

# Or explicitly:
# ruby2js: filters: functions, esm, pragma, return

The preset mode enables:

  • functions - Ruby method → JS method mappings
  • esm - ES module imports/exports
  • pragma - Line-level control via comments (e.g., # Pragma: ?? for nullish coalescing)
  • return - Implicit returns in methods
  • ES2022 features
  • Identity comparison (=====)

Summary

Pattern Works Well Needs Care
Classes ✓ Directly translates  
Methods ✓ Normal methods  
Private methods # prefix (ES2022) or _ prefix  
Arrays ✓ Most operations << needs pragma
Hashes ✓ Symbol keys .each needs pragma
Blocks ✓ Arrow functions Use noes2015 for this
Strings ✓ Interpolation  
Control flow ✓ if/unless/case  
Type checks   Avoid is_a?

See Anti-Patterns for patterns to avoid, and Pragmas for fine-grained control.

Next: Pragmas in Practice