Guide

# Installation

Add the gem, run the installer, register one Stimulus controller eagerly — the Rails engine wires up the endpoint and assets for you.

## Install

```ruby
gem "phlex-reactive"
```

```shell
bundle install
bin/rails generate phlex:reactive:install
```

The Rails engine automatically:

- mounts `POST /reactive/actions` → `Phlex::Reactive::ActionsController#create`
- adds the gem's `app/javascript` to the asset paths
- auto-pins (and `preload: true`s) the client controller for importmap apps

The **installer** (`phlex:reactive:install`) does the host-app wiring:

- registers the `reactive` Stimulus controller eagerly in your entrypoint
- writes `config/initializers/phlex_reactive.rb` with the common options

If you'd rather wire it by hand, register the controller once (eagerly — below).

## Generators

```shell
# Setup (idempotent)
bin/rails generate phlex:reactive:install

# Scaffold a state-backed component (record-less)
bin/rails generate phlex:reactive:component Counter increment decrement

# Scaffold a record-backed component (signed GlobalID identity)
bin/rails generate phlex:reactive:component Todos::Item toggle rename --record todo

# Custom signed state vars
bin/rails generate phlex:reactive:component Wizard next_step --state step open
```

The component generator also writes an RSpec spec when your app has a `spec/` directory.

## importmap-rails (default Rails 7+)

```javascript
import { application } from "controllers/application"
import ReactiveController from "phlex/reactive/reactive_controller"
application.register("reactive", ReactiveController)
```

> **Register eagerly, not lazily:** If you `lazyLoadControllersFrom`, the controller is fetched on first appearance — and a user who clicks immediately after load can fire before it connects, so nothing happens. Eager registration (above) guarantees it's bound before any interaction. The engine already pins and preloads the prebuilt minified build (~22 KB, not the ~106 KB commented source), so this adds no latency.

## esbuild / rollup / webpack (jsbundling)

```javascript
import { application } from "./application"
import ReactiveController from "phlex/reactive/reactive_controller"
application.register("reactive", ReactiveController)
```

If your bundler can't resolve the gem path, copy the file in. The gem ships a prebuilt minified module (`reactive_controller.min.js`, ~22 KB vs ~106 KB source) with a linked sourcemap — vendor that:

```shell
js="$(bundle show phlex-reactive)/app/javascript/phlex/reactive"
cp "$js/reactive_controller.min.js"     app/javascript/controllers/reactive_controller.js
cp "$js/reactive_controller.min.js.map"  app/javascript/controllers/reactive_controller.min.js.map
```

…and `import ReactiveController from "./reactive_controller"`.

Importing `reactive_controller.js` also registers the `reactive:visit` Turbo `StreamAction` that powers `reply.redirect`. The `import ReactiveController` above covers this; if you vendor the file, make sure it's actually imported (not tree-shaken away) or redirects won't fire.

## bun (bun-rails)

Same as esbuild. Point the import at the gem path or vendor the file.

## Requirements

- **Rails** ≥ 7.1
- **Phlex 2** via `phlex-rails`, with an `ApplicationComponent < Phlex::HTML` base class that includes the Phlex Rails helpers (`dom_id`, `t`, routes, etc.)
- **Turbo** ≥ 8 (for morphing) — `turbo-rails`, with `window.Turbo` available
- A `<meta name="csrf-token">` in your layout (standard Rails)
- **pgbus** (optional, recommended) — the reliable broadcast transport. See [Broadcasting](https://phlex-reactive.zoolutions.llc/docs/broadcasting) and [Transport: pgbus](https://phlex-reactive.zoolutions.llc/docs/transport-pgbus) to wire it up.

## Configuration

Create `config/initializers/phlex_reactive.rb` as needed:

```ruby
Phlex::Reactive.base_controller_name = "ApplicationController"   # CSRF + auth + Current
Phlex::Reactive.renderer             = ApplicationController     # app helpers during render
Phlex::Reactive.authorization_errors = [Pundit::NotAuthorizedError]
# Phlex::Reactive.action_path = "/_r/actions"                   # custom endpoint
# Phlex::Reactive.verifier    = ActiveSupport::MessageVerifier.new(ENV["REACTIVE_KEY"])
# Phlex::Reactive.flash_target = "flash"                         # DOM id reply…flash appends into
# Phlex::Reactive.flash_component = MyFlash                      # renders string flashes: new(level:, content:)
# Phlex::Reactive.error_flash  = ->(kind) { "Something went wrong (#{kind})." } # flash on endpoint failures
```

**error_flash** turns the failures the **endpoint** catches — bad token, default-deny, authorization, missing record (the `400`/`403`/`404` rescue paths) — into a turbo-stream flash the user sees, at the **same status** it already returns (statuses never change). The `kind` argument is one of `:tampered`, `:unknown_class`, `:not_reactive_class`, `:forbidden`, `:not_found`. It composes with `flash_target`/`flash_component` above.

### Observability & diagnostics

Three opt-in knobs help you see what the endpoint and client are doing. None of them change HTTP statuses or leak token/param values.

- `Phlex::Reactive.verbose_errors` — diagnostic endpoint error bodies + dropped-param logging (statuses never change). Defaults to `Rails.env.local?` — on in development **and** test, off in production.
- `Phlex::Reactive.log_events` — one compact line per reactive event (action/render/broadcast) at `DEBUG` — `[reactive] Counter#increment ok (3.1ms)`. Default off. The events fire for your APM regardless (`ActiveSupport::Notifications`, `*.phlex_reactive`); this flag only controls the gem’s own log lines.
- `Phlex::Reactive.debug` — client debug mode (devtools-lite). When on, every reactive root carries `data-reactive-debug="true"` and the console groups every dispatch — action, param **names** (never values), status, stream targets, round-trip ms. Off by default; gate it on `Rails.env.development?`.

If you change `action_path`, expose it to the client:

```erb
<meta name="phlex-reactive-action-path" content="<%= Phlex::Reactive.action_path %>">
```

### Client page-meta knobs

Two more `<meta>` tags tune the client runtime. There is **no** server-side setting for either — they live in your layout head.

```erb
<%# Client request timeout (default 30s). A hung request aborts client-side after %>
<%# this window (reactive:error kind "timeout"), so the per-component queue never wedges. %>
<meta name="phlex-reactive-timeout" content="15000"> <%# 15s, in ms %>

<%# Latency simulator — DEVELOPMENT ONLY. Exposes window.PhlexReactive.enableLatencySim(ms) / %>
<%# disableLatencySim() so you can actually SEE pending/optimistic affordances (~5ms locally). %>
<%= tag.meta(name: "phlex-reactive-env", content: "development") if Rails.env.development? %>
```

A timed-out POST may have **succeeded** server-side — phlex-reactive never auto-replays, so make retryable actions idempotent. With the latency simulator meta present, toggle it from the browser console: `PhlexReactive.enableLatencySim(400)` delays every action 400ms (persists to sessionStorage, clears when the tab closes). Without the meta there is no global handle and zero production surface.

### Custom param types

Register your own coercion for `action ..., params:` in the initializer. The block gets the raw client value and returns the coerced value, or `Phlex::Reactive::ParamSchema::DROP` to reject it (the keyword default then applies — the drop-don’t-fabricate contract).

```ruby
Phlex::Reactive.param_type(:money) do |value|
  /\A\d+(\.\d{1,2})?\z/.match?(value.to_s) ? BigDecimal(value) : Phlex::Reactive::ParamSchema::DROP
end

# then, in any component:
#   action :charge, params: { amount: :money }
#   def charge(amount:) = @invoice.charge!(amount) # a BigDecimal, or unset
```

> **Register during boot only:** The registry is **frozen after initialization** (the engine’s `after_initialize` calls `freeze_param_types!`), so a runtime `param_type` call raises. Declare every custom type in the initializer.

## Verify it works

Run the doctor — it validates the whole install (route, Stimulus registration, CSRF, the identity verifier, and every component) and prints `✓/✗/?` with a fix for each failure:

```shell
bin/rails phlex_reactive:doctor
```

Then drop a counter on any page, click `+`, and watch it increment with no full-page reload. If it reloads or does nothing, check the testing guide troubleshooting section.

> **Tip:** The doctor is read-only and safe to run anywhere. A `✗` points at the exact fix; a `?` is advisory (e.g. it can’t confirm csrf_meta_tags in a Phlex-only layout). No full-page reload on the click is the runtime signal everything is wired.