Process-backed components with their own state and lifecycle.
defmodule MyApp.Counter do
use Courgette.LiveComponent
def mount(_assigns), do: {:ok, %{count: 0}}
def render(assigns) do
box border: :rounded do
text(do: "Count: #{assigns.count}")
end
end
def handle_event({:key, :arrow_up}, assigns) do
{:noreply, update(assigns, :count, &(&1 + 1))}
end
def handle_event(_event, assigns), do: {:noreply, assigns}
end| Callback | Required | Returns |
|---|---|---|
mount/1 |
yes | {:ok, assigns} |
render/1 |
yes | Element.t() |
update/2 |
no | {:ok, assigns} |
handle_event/2 |
no | {:noreply, assigns} |
handle_info/2 |
no | {:noreply, assigns} |
terminate/2 |
no | any |
assign(assigns, %{key: value}) # merge map
assign(assigns, key: value) # merge keyword
assign(assigns, :key, value) # set one key
assign_new(assigns, :key, fn -> default end)
update(assigns, :key, &(&1 + 1))No process, no state. Pure function from assigns to element tree.
defmodule MyApp.UI do
use Courgette.Component
attr :name, :string, default: "World"
attr :color, :atom, default: :green
def greeting(assigns) do
assigns = assigns(assigns)
text color: assigns.color do
"Hello, #{assigns.name}!"
end
end
endassigns(keyword)— converts keyword list to map with defaults applied. Call at the top of every function component.theme(assigns, :token)— looks up a semantic color from the theme in assigns. Falls back toTheme.default().render_slot(assigns.slot_name)— renders a slot's content as a list of elements.attr/3— declares an attribute:attr :name, :type, opts. Types::string,:atom,:integer,:float,:boolean,:list,:map,:any.slot/1,2— declares a slot for passing child elements.
Embed a stateful child inside a parent's render/1:
def render(assigns) do
box do
live_component(Counter, id: "main", initial_count: 5)
live_component(Counter, id: "other", focusable: true)
end
end:idis required — used for lifecycle reconciliation as{module, id}.:focusable— set totrueto include in Tab-cycling focus order.- All other options are passed as props to
mount/1(first render) orupdate/2(re-render).
Children notify parents via send/2:
# In child's handle_event:
send(assigns.parent_pid, {:counter_changed, assigns.count})
# In parent's handle_info:
def handle_info({:counter_changed, count}, assigns) do
{:noreply, assign(assigns, :child_count, count)}
endassigns.parent_pid is automatically set by the runtime for child components.
The runtime matches children by {module, id} key:
- Same key, new props → calls
update/2on the existing child - New key → starts a new child process via
mount/1 - Removed key → stops the child process
This means:
- IDs must be unique per module within a parent
- Changing the
:idprop destroys and recreates the component - The order of
live_componentcalls determines focus order
- Forgetting
:idonlive_component/2— will raise at runtime. - Non-unique IDs within the same module — causes child conflicts.
- Returning
{:noreply, assigns}frommount/1— mount must return{:ok, assigns}. - Calling
assigns/1in a LiveComponent —assigns/1is for function components only. LiveComponents receive assigns as a map directly.