Patterns
Patterns
This guide covers patterns that work well when writing Ruby code for JavaScript. These patterns apply whether you’re writing dual-target code (runs in both Ruby and JavaScript) or JavaScript-only code.
Table of Contents
- Patterns
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 uses parentheses to distinguish between property access and method calls. This is one of the most important concepts.
# No parens = property access (getter)
len = obj.length
first = arr.first
# Empty parens = method call
item = list.pop()
result = obj.process()
# Parens with args = always method call
obj.set(42)
This applies to your own methods too. When defining methods:
def foo— becomes a getter, accessed asobj.foodef foo()— becomes a method, called asobj.foo()
When calling methods (especially in callbacks):
class Widget
def setup()
# WRONG: increment without parens just returns the getter
# @button.addEventListener('click') { increment }
# RIGHT: use parens to actually call the method
@button.addEventListener('click') { increment() }
end
def increment()
@count += 1
end
end
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
Range Indexing
Ruby’s range indexing translates to JavaScript’s slice():
str = "hello"
str[0..-2] # all but last char
str[2..-1] # from index 2 to end
str[1..3] # characters 1-3 (inclusive)
str[0...-2] # exclusive range (up to but not -2)
This works for both strings and arrays.
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")
Modules
Ruby’s module keyword works for namespacing in both Ruby and JavaScript:
module Utils
def self.format(str)
str.upcase
end
end
Modules with constants use an IIFE pattern:
module Config
VERSION = "1.0"
def self.load
# ...
end
end
Nested modules work too:
module App
module Models
class User
end
end
end
Organizing Files
For dual-target code, the require filter bundles multiple files:
# main.rb
require_relative 'utils'
require_relative 'config'
# Works in Ruby, inlined for JavaScript
This pattern works in Ruby natively and produces a single bundled JavaScript file. See Require Filter for details.
For JavaScript-only code, use ESM imports instead. See JavaScript-Only.
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
Recommended Filters
The most commonly used filters:
- functions - Ruby method → JS method mappings (
.select→.filter, etc.) - pragma - Line-level control via comments
- return - Implicit returns in methods
- require - Bundle multiple files (for dual-target code)
For JavaScript-only code, add:
- esm - ES module imports/exports
The preset option enables sensible defaults:
# ruby2js: preset
See Options for configuration details.
Summary
| Pattern | Works Well | Needs Care |
|---|---|---|
| Classes | ✓ Directly translates | |
| Methods | ✓ Normal methods | Parens matter |
| Modules | ✓ Namespacing | |
| Private methods | ✓ # prefix (ES2022) or _ prefix |
|
| Arrays | ✓ Most operations | << needs pragma |
| Range indexing | ✓ [0..-2] → .slice() |
|
| 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, Pragmas for fine-grained control, and JavaScript-Only for ESM, async/await, and JavaScript APIs.