Filament.Hooks (filament v0.2.1)

Copy Markdown

Hooks for Filament components.

Application-facing hooks

Call these at the top level of render/1:

  • use_state/1 — local mutable state; returns {value, setter}
  • use_observable/1 — resolves a server reference to a pid (or nil when disconnected)
  • use_observable/2 — resolves a server and projects its state; fn receives :disconnected when unavailable
  • use_effect/2 — side-effect with optional cleanup
  • memo_at/3 and event_at/2 — invoked by compiler-generated code from ~F templates

Pattern: use_observable/1 + use_observable/2

Resolve the server once with /1, then project from it with /2. This lets you pass the server pid to child components and apply multiple projections from the same process:

def render(%{session_id: session_id}) do
  server = use_observable(fn -> MyServer.start_link(session_id) end)
  count  = use_observable(server, fn
    :disconnected -> 0
    state -> state.count
  end)
  label  = use_observable(server, fn
    :disconnected -> ""
    state -> state.label
  end)
  ...
end

Passing the server as a prop lets child components project their own values without creating redundant subscriptions:

<ChildComponent server={server} />

# In the child:
def render(%{server: server}) do
  value = use_observable(server, fn
    :disconnected -> nil
    s -> s.some_field
  end)
  ...
end

Rules of hooks

  1. Only call hooks at the top level of render/1 — not inside if, case, or comprehensions.
  2. Hooks must be called during a render pass (a RenderContext must be active).
  3. Hook identity is determined by call order (slot index). Conditional hooks corrupt state.

Summary

Functions

Schedules a side effect to run after the render completes.

Resolves an observable server reference to a pid, without subscribing.

Resolve an observable server and project its state into a value.

Returns the current state value and a setter function.

Functions

use_effect(effect_fn, deps)

@spec use_effect((-> (-> term()) | nil), deps :: [term()] | :always) :: :ok

Schedules a side effect to run after the render completes.

effect_fn is called after the component renders. It may return a cleanup function (zero-arity fn returning :ok or any term) that is called:

  • Before the next time the effect runs (when deps change), OR
  • When the fiber unmounts

deps controls when the effect re-runs:

  • [] — run once on mount, cleanup on unmount
  • [dep1, dep2] — run when any dep changes (Kernel.== comparison), cleanup before re-run
  • :always — run on every render

Rules: call only at the top level of render/1.

use_observable(server_or_fn)

@spec use_observable(
  server_or_fn ::
    GenServer.server() | (-> pid() | {:ok, pid()} | GenServer.server())
) :: pid() | GenServer.server() | nil

Resolves an observable server reference to a pid, without subscribing.

The argument can be any of:

  • a pid, atom, {:via, ...}, or {node, name} — used directly as the server
  • a zero-arity function — called on first connect (and again if the process dies) to obtain a pid or {:ok, pid}; useful when the component owns the server's lifecycle

Returns nil during disconnected (HTTP static) mounts. On subsequent renders, reuses an existing pid if still alive; restarts a factory fn otherwise.

Use this hook when you want to pass the server identity to child components or apply multiple projections from the same server via use_observable/2.

Must be called at the top level of render/1 in consistent order (like all hooks).

use_observable(server_or_fn, project)

@spec use_observable(
  server_or_fn ::
    GenServer.server() | (-> pid() | {:ok, pid()} | GenServer.server()),
  project :: (term() | :disconnected -> term())
) :: term()

Resolve an observable server and project its state into a value.

The first argument is a server reference (same as use_observable/1). The second argument is a projection function called on every state update from the server. When the server is unavailable (disconnected HTTP mount or nil), the function is called with the atom :disconnected so it can return a safe default:

count = use_observable(CartServer, fn
  :disconnected -> 0
  state        -> state.count
end)

Passing the server as a prop lets a parent resolve the process once and share it with children that each apply their own projection:

# Parent
server = use_observable(fn -> MyServer.start_link([]) end)
<Child server={server} />

# Child
value = use_observable(server, fn
  :disconnected -> nil
  s             -> s.some_field
end)

Must be called at the top level of render/1 in consistent order (like all hooks). Do not call inside conditionals or loops.

use_state(initial)

@spec use_state(initial :: term()) :: {value :: term(), setter :: (term() -> :ok)}

Returns the current state value and a setter function.

On the first render of this fiber, returns {initial, setter}. On subsequent renders, returns the most recently set value (or initial if never changed).

The setter is a closure. Call it from event handlers or effects to trigger a re-render. Calling the setter sends a message to the owning LiveView process, which re-renders the affected fiber.

Rules: call only at the top level of render/1. Do not call inside conditionals.