`attr` turns component bugs into compile errors

almirsarajcic

almirsarajcic

1 hour ago

0 comments

A function component is just a function taking assigns, which means a typo’d or forgotten assign fails the way any missing map key fails: at render time, in production, on whichever page happens to call it wrong. attr and slot move that check to compile time, and they check the call site — the place that actually has the bug.

defmodule MyAppWeb.Components do
  use Phoenix.Component

  attr :user, :map, required: true
  attr :size, :string, default: "md", values: ~w(sm md lg)
  slot :actions

  def user_card(assigns) do
    ~H"""
    <div class={"card card-#{@size}"}>
      <span>{@user.name}</span>
      <div :for={action <- @actions}>{render_slot(action)}</div>
    </div>
    """
  end
end

Call it wrong and the compiler tells you before you ever load the page:

<.user_card size="xl" />
warning: missing required attribute "user" for component MyAppWeb.Components.user_card/1
warning: attribute "size" in component MyAppWeb.Components.user_card/1 must be one of ["sm", "md", "lg"], got: "xl"

Both warnings point at the caller’s line, not at the component. That is the whole value: the component is fine, the call site is wrong, and the error names the call site.

What each option buys you

required: true catches the forgotten assign. default: removes the assigns[:size] || "md" dance from the body — the default is applied before your function runs, so @size is always set. values: is the one people miss: it turns a free-form string into a closed set, so a renamed variant produces warnings at every stale call site instead of silently rendering a card-xl class that no CSS defines.

Typing the attribute as a struct tightens it further:

attr :user, MyApp.Accounts.User, required: true
attr :on_close, :any, default: nil
attr :rest, :global, include: ~w(form target)

:global is what lets callers pass phx-click, data-*, or aria-* straight through to your root element via {@rest} without you declaring each one — and include: extends the allowed set for attributes Phoenix does not treat as global by default.

Slots get the same treatment

slot :inner_block, required: true

slot :column, doc: "a table column" do
  attr :label, :string, required: true
  attr :sortable, :boolean
end

Attributes declared inside a slot block are validated on each slot entry, so <:column> without a label reports the same way:

warning: missing required attribute "label" in slot "column" for component MyAppWeb.Components.table/1

There is a second payoff beyond correctness. Because these declarations are introspectable, mix docs renders a full attribute table for every component automatically — the documentation stops being a thing you maintain by hand and starts being a thing you cannot forget to update.

The migration is incremental. Adding attr to one component changes nothing for the others, and each one you annotate immediately starts reporting its own bad call sites.

Phoenix.Component.attr/3 docs

Comments (0)

Sign in with GitHub to join the discussion