Phlex support
Phlex support comes from the gem hibiki_phlex: reactive components. A Phlex component is a plain Ruby object, so Hibiki::Reactive gives it per-instance signals read as ordinary method calls — no .value anywhere in view_template — and a render effect re-renders it whenever one of those signals changes.
signal write → render effect re-runs → component re-renders
(same instance) → fresh HTML handed to your block
Transport-agnostic on purpose: the gem depends only on hibiki and phlex. The block receiving the HTML decides where it goes — broadcast it over Turbo Streams, transmit it down a channel subscription, write it to a file, diff it in a test.
Requires Phlex >= 2.4, < 2.5 (see the version-pin rationale below) and Ruby >= 3.4:
# Gemfile
gem "hibiki_phlex"
Usage
class TodoList < Phlex::HTML
include Hibiki::Reactive
include Hibiki::Phlex::Rerenderable
state(:items) { [] }
derived(:remaining) { items.count { |item| !item[:done] } }
def view_template
div(id: "todos") do
h2 { "Todos — #{remaining} remaining" }
ul { items.each { |item| li { item[:title] } } }
end
end
# Signals compare with ==; build new values instead of mutating in place.
def add(title) = self.items = items + [{ title:, done: false }]
end
list = TodoList.new
effect = Hibiki::Phlex.render_effect(list) do |html|
# your transport — e.g. inside a Hibiki::Rails::Channel:
# broadcast_replace target: "todos", html:
end
list.add("write docs") # → the block runs again with fresh HTML
effect.dispose # or let an enclosing Hibiki.root own teardown
- The effect’s first run is the dependency-collecting initial render: signals read inside
view_templatesubscribe it through plain method calls, and that first HTML is yielded too. - Re-renders happen on the same instance — signal identity lives in the instance, so a fresh instance per render would reset every signal. That’s what
Rerenderableexists for. render_effect(component, scheduler:)passes the scheduler through toHibiki::Effect, so reruns can be deferred or debounced (e.g.Hibiki::Rails::Debounce).- Returns the
Hibiki::Effect. Created insideHibiki.root(or another effect), the owner tree disposes it automatically; a bare caller calls#dispose.
With hibiki_rails
The combination is the LiveView-ish loop — one signal graph per cable connection, one render effect per component:
class TodosChannel < ApplicationCable::Channel
include Hibiki::Rails::Channel
def build_graph
@list = TodoList.new
Hibiki::Phlex.render_effect(@list) do |html|
broadcast_replace target: "todos", html:
end
end
def add(data) = @list.add(data["title"])
end
Granularity is inherent and worth knowing: the ERB style in hibiki_rails is one effect per partial (fine-grained), a Phlex render effect is one effect per component (component-grained). Both are correct; pick per page. The initial-state pattern (server-rendered placeholder + subscribe after the Turbo stream confirms) is transport-side — see Rails usage. On the transmit transport there is no such ordering problem (see The JS client).
Why the strict Phlex pin
Phlex components are one-shot: an instance renders once, and a second call raises Phlex::DoubleRenderError. Phlex 2 tracks “spent” purely through the private @_state ivar — everything else about a render is per-call — so Rerenderable#rerender clears it and calls again. That is the entire adapter, but it leans on a private implementation detail, so:
- the gemspec allows only verified Phlex minors (
>= 2.4, < 2.5), and - a contract spec pins the upstream behavior itself, so bumping the bound fails loudly in CI if the internals moved.