Coming from React

If you know React, you’ll find Ruby2JS provides a familiar component model with cleaner syntax.

Table of Contents

What You Know → What You Write

React (JavaScript) Ruby2JS
useState(0) @count = 0
const [count, setCount] = useState(0) count, setCount = useState(0)
useEffect(() => {}, []) useEffect(-> {}, [])
{count} (JSX) {count} (JSX via %x{})
onClick={() => setCount(c => c + 1)} onClick: -> { setCount(->(c) { c + 1 }) }
export default function Counter() def Counter() (auto-exported)

Quick Start

Try it live — edit the Ruby code and see the JavaScript output:

def Counter(initial: 0)
  count, setCount = useState(initial)

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

Component Patterns

Functional Components with Hooks

def UserProfile(user_id:)
  user, setUser = useState(nil)
  loading, setLoading = useState(true)

  useEffect -> {
    fetch("/api/users/#{user_id}")
      .then(->(r) { r.json })
      .then(->(data) {
        setUser(data)
        setLoading(false)
      })
  }, [user_id]

  return %x{<p>Loading...</p>} if loading

  %x{
    <div className="profile">
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  }
end

Event Handlers

def Form()
  name, setName = useState("")

  handleSubmit = ->(e) {
    e.preventDefault()
    console.log("Submitted: #{name}")
  }

  %x{
    <form onSubmit={handleSubmit}>
      <input value={name} onChange={->(e) { setName(e.target.value) }} />
      <button type="submit">Submit</button>
    </form>
  }
end

Custom Hooks

def useLocalStorage(key, initial_value)
  stored = localStorage.getItem(key)
  value, setValue = useState(stored ? JSON.parse(stored) : initial_value)

  useEffect -> {
    localStorage.setItem(key, JSON.stringify(value))
  }, [key, value]

  [value, setValue]
end

# Usage
def Settings()
  theme, setTheme = useLocalStorage("theme", "light")
  # ...
end

Why Ruby2JS for React?

The syntax improvements are nice, but the real value is what Ruby brings beyond JSX:

Full-Stack Ruby

Same language on frontend and backend. If you know Rails, the patterns transfer:

# Backend model (Rails)
class Post < ApplicationRecord
  validates :title, presence: true
  has_many :comments
end

# React component (Ruby2JS)
def PostList()
  posts, setPosts = useState([])

  useEffect -> {
    Post.published.order(created_at: :desc).then { |p| setPosts(p) }
  }, []
  # ...
end

Rails Ecosystem

ActiveRecord queries, validations, associations—directly in your React components:

def PostPage()
  post = useLoaderData()  # From server: Post.find(params[:id])
  comments = post.comments.includes(:author)
  related = Post.where(category: post.category).limit(3)
  # ...
end

Syntax Benefits

The syntax improvements add up across a codebase:

# Cleaner string interpolation
"Hello, #{user.name}!"  # vs `Hello, ${user.name}!`

# Implicit returns
double = ->(n) { n * 2 }  # vs const double = (n) => n * 2

# Cleaner conditionals
return %x{<Loading />} if loading
%x{<Content data={data} />}

Key Differences

JSX Syntax

Ruby2JS uses %x{} blocks for JSX:

def Example(title:, content:, value:)
  # Inline JSX
  %x{<Component prop={value} />}

  # Multi-line works naturally
  %x{
    <div>
      <h1>{title}</h1>
      <p>{content}</p>
    </div>
  }
end

Auto-Export

When your file defines exactly one function or class, it’s automatically exported as default:

def MyComponent(name:)
  %x{<h1>Hello, {name}!</h1>}
end

Props vs Instance Variables

In React components, use props directly. Instance variables (@) are for class components:

# Functional component (recommended)
def Greeting(name:)
  %x{<h1>Hello, {name}!</h1>}
end

# Class component (if needed)
class Counter < React::Component
  def initialize
    @count = 0  # Instance variable becomes this.state
  end
end

Next Steps

Back to Juntos/coming-from