Project

tombolo

0.0
No release in over 3 years
Lightweight alternative to react-rails for mounting React components in Rails views with optional server-side rendering via ExecJS.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 7.0
 Project Readme

Tombolo

Gem Version npm CI MIT License

Tombolo is a lightweight alternative to react-rails for mounting React islands in your Rails views. It provides a react_component view helper, client-side mounting via a component map, and optional server-side rendering through ExecJS.

Built for React 18+, Tombolo targets modern Rails apps using jsbundling-rails (or any JS bundler) and propshaft, with no asset pipeline integration — just register your components and write a few lines of JavaScript.

Compatibility

Dependency Versions
Ruby >= 3.2
Rails >= 7.0
React 18, 19

Installation

Add the gem to your Gemfile:

gem "tombolo"
# gem "execjs"  # required for server-side rendering

Install the npm package:

npm install tombolo
# or
pnpm add tombolo

Optionally, run the install generator to create a config initializer (config/initializers/tombolo.rb) and an SSR entry point (app/javascript/prerender.ts):

bin/rails generate tombolo:install

Usage

Rails helper

Render a React component from any view or partial:

<%= react_component("Greeting", props: { name: "World" }) %>

This renders a <div> with data-react-component and data-react-props attributes that the JavaScript side picks up.

Client-side mounting

Tombolo takes a component map — an object mapping component names to React components. The easiest way to create one is a barrel file (a module that re-exports from other modules):

// app/javascript/components/index.ts
export { Greeting } from "./Greeting";
export { SearchForm } from "./SearchForm";

With Turbo

For apps using Turbo, use Tombolo.mount and Tombolo.unmount directly:

import * as Tombolo from "tombolo";
import * as components from "./components";

document.addEventListener("turbo:load", () => Tombolo.mount(components));
document.addEventListener("turbo:before-cache", () => Tombolo.unmount());

Without Turbo

For apps without Turbo, Tombolo.start mounts components on DOMContentLoaded (or immediately if the DOM is already loaded):

import * as Tombolo from "tombolo";
import * as components from "./components";

Tombolo.start(components);

Scoped mounting

Both Tombolo.mount and Tombolo.unmount accept an optional scope parameter to limit operations to a subtree of the DOM:

Tombolo.mount(components, document.getElementById("sidebar"));

Server-side rendering

SSR is optional and requires the execjs gem.

Create a server entry point that registers your components:

// app/javascript/prerender.ts
import { registerServerRenderer } from "tombolo/server";
import * as components from "./components";

registerServerRenderer(components);

Build it with esbuild (or your bundler of choice) as a CommonJS bundle that ExecJS can evaluate:

esbuild app/javascript/prerender.ts --bundle --platform=neutral --outfile=app/assets/builds/prerender.js

Then use prerender: true in your views:

<%= react_component("Greeting", props: { name: "World" }, prerender: true) %>

When a component is prerendered, the server-rendered HTML is placed inside the div and Tombolo uses hydrateRoot instead of createRoot on the client. This preserves interactivity without a full re-render.

The default server bundle path is app/assets/builds/prerender.js. To customize it, add an initializer:

# config/initializers/tombolo.rb
Tombolo.configure do |config|
  config.server_bundle = "path/to/your/bundle.js"
end

Named render scopes

If your app needs multiple SSR bundles with different sets of components, you can register named render scopes. Each scope gets its own isolated ExecJS runtime:

Tombolo.configuration.server_bundles[:admin] = "app/assets/builds/admin/prerender.js"
Tombolo.configuration.server_bundles[:storefront] = "app/assets/builds/storefront/prerender.js"

Then reference the scope by name:

<%= react_component("Dashboard", prerender: :admin, props: { ... }) %>
<%= react_component("ProductGrid", prerender: :storefront, props: { ... }) %>
<%= react_component("Greeting", prerender: true, props: { ... }) %>  <%# uses :default %>

prerender: true uses the :default bundle. prerender: :name uses a named bundle.

Configuration

Tombolo.configure do |config|
  # Convert snake_case prop keys to camelCase (default: false)
  config.camelize_props = true

  # Path to the default server-side JS bundle for SSR
  # (default: "app/assets/builds/prerender.js")
  config.server_bundle = "app/assets/builds/prerender.js"

  # Named server bundles for isolated SSR render scopes
  # config.server_bundles[:admin] = "path/to/admin/prerender.js"
end

camelize_props can also be overridden per-call:

<%= react_component("Greeting", props: { first_name: "World" }, camelize_props: true) %>

API reference

JavaScript

mount(components, scope?)

Scans scope (default: document) for elements with a data-react-component attribute. For each element, looks up the component by name, parses props from data-react-props, and mounts it. Elements with a data-react-prerender attribute are hydrated with hydrateRoot; all others use createRoot. Already-mounted elements are skipped.

unmount(scope?)

Unmounts all tracked React roots within scope (default: document).

start(components)

Calls mount on DOMContentLoaded, or immediately if the DOM is already loaded.

registerServerRenderer(components)

Imported from tombolo/server.

Assigns a renderComponent(name, propsJson) function to globalThis, making it callable from ExecJS. Used in server entry points for SSR.

Ruby

react_component(name, props: {}, prerender: false, camelize_props: nil)

Renders a <div> with data-react-component and data-react-props attributes. When prerender: true, the component is rendered on the server via ExecJS using the :default bundle. Pass a Symbol (e.g. prerender: :admin) to use a named render scope instead. Pass camelize_props: true to convert snake_case prop keys to camelCase, or set it globally in the configuration.

Tombolo.configure { |config| ... }

See Configuration above.

Migrating from react-rails

Helper signature. react-rails passes props as a positional argument, Tombolo uses a keyword argument:

# react-rails
react_component("Name", { title: "Hello" })

# Tombolo
react_component("Name", props: { title: "Hello" })

No html_options argument. react-rails accepts a third argument for HTML attributes on the wrapper div. Tombolo does not support this — wrap the helper call in your own tag if you need custom attributes.

No asset pipeline integration. Tombolo does not ship a component generator or integrate with Sprockets/Webpacker. You manage your JavaScript build yourself with jsbundling-rails or an equivalent.

SSR setup. Replace any server_rendering.js pack with a prerender.ts entry point that calls registerServerRenderer. See Server-side rendering above.

Contributing

Bug reports and pull requests are welcome on GitHub.

License

MIT