React setup for Rails engine gems that follow keystone_ui's design: a helper that mounts a React UI on a page, a registry of the UIs an engine draws, and React controls styled with keystone_ui-styles classes.
Host apps do not install this gem directly. An engine with a React UI depends on it, and the host gets it with that engine.
- Rails 7.0 or later
- React 19 or later, in the engine that draws the UI
- keystone_ui-styles, which keystone_ui brings into the host's Tailwind build
Two things, from the same repo and the same version. The gem carries the mount helper:
spec.add_dependency "keystone_ui-react"The npm package carries the controls and the registry, and is installed from the git tag rather than from the npm registry:
"dependencies": {
"keystone_ui-react": "github:DYB-Development/keystone_ui-react#v0.1.0"
}The gem ships React already built, and the page loads it once however many engines draw React UIs on it:
<%= javascript_include_tag "keystone_ui/react", defer: true %>Each engine builds its own script without React in it, taking React and the DOM client from the page instead:
esbuild app/javascript/alembic/page_builder.jsx --bundle --format=iife \
--alias:react=keystone_ui-react/src/react_on_page.js \
--alias:react-dom/client=keystone_ui-react/src/react_dom_on_page.js \
--alias:react-dom=keystone_ui-react/src/react_dom_on_page.js \
--alias:react/jsx-runtime=keystone_ui-react/src/jsx_runtime_on_page.jsThe last alias is for packages an engine depends on that draw with React's
automatic runtime rather than with createElement.
The page loads the gem's script before any engine's script, since an engine's script reads React off the page as it runs.
An engine registers each UI under a name and mounts every one the page carries:
import { createRoot } from "react-dom/client"
import { register } from "keystone_ui-react/src/registry.js"
import { startMounting } from "keystone_ui-react/src/mounting.js"
import PageBuilder from "./page_builder/PageBuilder"
register("alembic/page-builder", PageBuilder)
startMounting(document, createRoot)startMounting draws the UIs the page already carries, draws them again on a
Turbo visit, and takes them down before Turbo keeps a copy of the page, so a UI
appears whether its page was loaded fresh, reached by a link, or gone back to.
The view writes the element that UI mounts into, with the props it draws from:
<%= react_ui("alembic/page-builder", name: @page.name) %>react_ui comes from KeystoneUi::React::MountHelper, which a controller adds
the way it adds any helper:
require "keystone_ui/react/mount_helper"
class BuilderController < ApplicationController
helper KeystoneUi::React::MountHelper
endAnything else passed to react_ui lands on the element, so a UI can be given
classes or its own data attributes.
Each one takes keystone's classes and passes everything else through to the element it draws:
Alert, Badge, Button, Card, Checkbox, Input, Page, PageHeader,
Panel, Section, Select, Textarea, and the form field text Label,
Hint, FieldError and Required.
import Button from "keystone_ui-react/src/Button"
<Button variant="secondary" size="sm" onClick={save}>Save</Button>They import nothing but React and each other, so an engine takes on no other dependency by using them, and their look comes from keystone_ui-styles classes on the page.
bundle install
npm install
bundle exec rake test # the mount helper
npm test # the controls and the registry
bin/rubocopSee RELEASING.md. A release is not usable by an engine until its git tag is on GitHub, because that tag is what the npm package resolves.
MIT. See MIT-LICENSE.