Coming from Next.js

If you know Next.js, you’ll find Ruby2JS provides similar file-based routing and React components—with Ruby syntax.

Table of Contents

What You Know → What You Write

Next.js Ruby2JS
pages/index.js app/pages/index.jsx.rb
pages/posts/[id].js app/pages/posts/[id].jsx.rb
pages/blog/[...slug].js app/pages/blog/[...slug].jsx.rb
getStaticProps # Pragma: revalidate 60
useRouter() router = useRouter()
router.push('/path') router.push('/path')

Quick Start

1. File-based routing works the same:

app/pages/
  index.jsx.rb          → /
  about.jsx.rb          → /about
  posts/
    index.jsx.rb        → /posts
    [id].jsx.rb         → /posts/:id
    [...slug].jsx.rb    → /posts/*slug

2. Create a page:

# app/pages/posts/[id].jsx.rb
def PostPage()
  router = useRouter()
  id = router.query[:id]
  post, setPost = useState(nil)

  useEffect -> {
    fetch("/api/posts/#{id}")
      .then(->(r) { r.json })
      .then(->(data) { setPost(data) })
  }, [id]

  return %x{<p>Loading...</p>} unless post

  %x{
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML= />
    </article>
  }
end

3. The generated React component:

export default function PostPage() {
  const router = useRouter();
  const id = router.query.id;
  const [post, setPost] = useState(null);

  useEffect(() => {
    fetch(`/api/posts/${id}`)
      .then(r => r.json())
      .then(data => setPost(data));
  }, [id]);

  if (!post) return <p>Loading...</p>;

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML= />
    </article>
  );
}

Routing Patterns

Dynamic Routes

# app/pages/users/[id].jsx.rb
def UserProfile()
  router = useRouter()
  id = router.query[:id]
  # ...
end

# app/pages/posts/[...slug].jsx.rb (catch-all)
def BlogPost()
  router = useRouter()
  slug = router.query[:slug]  # Array of path segments
  # ...
end
def Navigation()
  router = useRouter()

  go_to_post = ->(id) {
    router.push("/posts/#{id}")
  }

  go_back = -> {
    router.back()
  }

  %x{
    <nav>
      <button onClick={go_back}>Back</button>
      <button onClick={-> { go_to_post(123) }}>View Post</button>
    </nav>
  }
end
import { Link } from 'next/link'

def Header()
  %x{
    <header>
      <Link href="/">Home</Link>
      <Link href="/about">About</Link>
      <Link href="/posts/123">Post 123</Link>
    </header>
  }
end

Data Fetching

Client-Side Fetching

def Dashboard()
  data, setData = useState(nil)
  loading, setLoading = useState(true)

  useEffect -> {
    fetch('/api/dashboard')
      .then(->(r) { r.json })
      .then(->(d) {
        setData(d)
        setLoading(false)
      })
  }, []

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

  %x{
    <div>
      <h1>Dashboard</h1>
      <p>Welcome, {data.user.name}</p>
    </div>
  }
end

ISR (Incremental Static Regeneration)

Use pragmas to enable ISR:

# Pragma: revalidate 60

def PostsList()
  # This page will be regenerated every 60 seconds
  posts, setPosts = useState([])

  useEffect -> {
    fetch('/api/posts')
      .then(->(r) { r.json })
      .then(->(data) { setPosts(data) })
  }, []

  %x{
    <ul>
      {posts.map { |post| <li key={post.id}>{post.title}</li> }}
    </ul>
  }
end

API Routes

# app/api/posts.rb
def handler(req, res)
  if req.method == 'GET'
    posts = Post.all
    res.status(200).json(posts)
  elsif req.method == 'POST'
    post = Post.create(req.body)
    res.status(201).json(post)
  else
    res.status(405).end()
  end
end

Why Ruby2JS for Next.js?

Next.js gives you file-based routing and React. Ruby2JS adds the Rails ecosystem:

Full-Stack Ruby

Same language in your pages, API routes, and backend. Rails patterns everywhere:

# Backend model (Rails)
class Post < ApplicationRecord
  validates :title, presence: true
  scope :published, -> { where(published: true) }
end

# API route (Ruby2JS)
def handler(req, res)
  posts = Post.published.order(created_at: :desc)
  res.status(200).json(posts)
end

Built-in ORM

ActiveRecord in your API routes—no separate ORM to learn:

# app/api/posts/[id].rb
def handler(req, res)
  post = Post.find(req.query[:id])

  case req.method
  when 'GET'
    res.json(post.as_json(include: :comments))
  when 'PUT'
    post.update(req.body)
    res.json(post)
  when 'DELETE'
    post.destroy
    res.status(204).end()
  end
end

Rails Ecosystem

Validations, associations, scopes—the full ActiveRecord toolkit:

@post = Post.find(id)
@comments = @post.comments.includes(:author).order(created_at: :desc)
@related = Post.where(category: @post.category).published.limit(3)

Syntax Benefits

Cleaner Ruby syntax throughout:

# Conditionals
return %x{<Loading />} if loading

# String interpolation
"/posts/#{post[:id]}"

# Blocks
posts.select { |p| p[:published] }.map { |p| %x{<Post {...p} />} }

Key Differences

JSX Syntax

Ruby2JS uses %x{} for JSX:

# Multi-line JSX
%x{
  <div className="container">
    <Header />
    <main>{children}</main>
    <Footer />
  </div>
}

# Inline JSX
%x{<Button onClick={handleClick}>Click Me</Button>}

File Extensions

Use .jsx.rb for React/Next.js pages:

pages/           →  app/pages/
  index.js       →    index.jsx.rb
  about.js       →    about.jsx.rb
  posts/[id].js  →    posts/[id].jsx.rb

Query Parameters

Access via router object:

router = useRouter()
id = router.query[:id]
page = router.query[:page] || 1

Layout Pattern

# app/layouts/default.jsx.rb
def Layout(children:)
  %x{
    <div className="layout">
      <Header />
      <main>{children}</main>
      <Footer />
    </div>
  }
end

# app/pages/index.jsx.rb
def HomePage()
  %x{
    <Layout>
      <h1>Welcome</h1>
      <p>This is the home page.</p>
    </Layout>
  }
end

Next Steps

Back to Juntos/coming-from