Filament – React-style hooks and observable GenServers for Phoenix LiveView
I just published Filament, a component and state-management layer for Phoenix LiveView that brings a few ideas from React into Elixir — without the JavaScript.
The problem it solves
As LiveView apps grow, state tends to scatter: socket assigns accumulate, handle_event callbacks multiply, and PubSub wiring ends up duplicated across LiveViews. When multiple components care about the same GenServer, you end up with a lot of boilerplate to subscribe, receive, and broadcast updates.
Filament gives you a cleaner model: isolated component fibers, hook-based local state, and observable GenServers that components subscribe to directly.
What it looks like
defmodule CartWeb.Components.CartBadge do
use Filament.Component
defcomponent do
prop(:cart_id, :string, required: true)
def render(%{cart_id: cart_id}) do
# subscribes to the GenServer; re-renders only when item count changes
count = use_observable({:via, Registry, {Cart.Registry, cart_id}}, fn
:disconnected -> 0
state -> Cart.State.item_count(state)
end)
~F"""
<span class="badge">{count} items</span>
"""
end
end
end
use_observable/2 subscribes to any Observable.GenServer. When the server calls notify_observers(new_state), every subscribed component re-renders — no handle_info, no PubSub wiring in the LiveView. The projection fn extracts only the slice the component cares about; if the projected value is unchanged, the re-render is suppressed.
Key features
~Ftemplates — JSX-like sigil with{expression}interpolation,{for … do}loops, and<MyComponent prop={value} />child tagsdefcomponent— typed, validated props; each instance gets an isolated fiber with its own hook state and event handlersuse_state/1— local mutable state that re-renders only the affected fiber, never the whole LiveView- Observable GenServers — wrap any GenServer with
use Filament.Observable.GenServer; components subscribe withuse_observable/2 - Projections + change-or-bust — projection fns run at render time and can close over local component state; stable projected values suppress re-renders automatically
- Automatic memoization — the
~Fcompiler wraps closures and child renders inmemo_atcalls; stable subtrees skip re-evaluation without any annotation - Composable custom hooks — any function that calls
use_state,use_observable, oruse_effectis a hook; domain logic lives in plain module functions use_effect/2— side effects with dependency tracking and cleanup- Static render with seamless handoff — subscriptions run during the initial HTTP render so pages arrive with real data already in the HTML; the WebSocket connection inherits the existing subscription without re-fetching
- Incremental adoption — drop a Filament tree into an existing LiveView with
Filament.LiveComponent; no big-bang rewrite required - Fast, in-process tests — mount and interact with component trees without a browser or WebSocket; tests run
async: truein milliseconds
Getting started
{:filament, "~> 0.2"}
- Docs: filament v0.2.1 — Documentation
- Hex: filament | Hex
- GitHub:
There are four example apps in the repo (todo, cart, inventory, collaboration) and guides covering getting started, observables, hooks, and incremental migration.
Early days — feedback very welcome.






















