Rbtz.CredoChecks.Warning.PhxHookComponentWithoutStableId (rbtz_credo_checks v0.2.0)

Copy Markdown View Source

Basics

This check is disabled by default.

Learn how to enable it via .credo.exs.

This check has a base priority of high and works with any version of Elixir.

Explanation

Requires function components whose template uses phx-hook to bind the hook target element to a stable DOM id.

Phoenix LiveView hooks are keyed by DOM id, so every hook target must have a stable, unique id. If the id can be missing or nil at runtime, multiple instances of the same component collide and LiveView can't route pushEvents or maintain the hook across patches.

A hook target's id is considered stable when it is any of:

  • a literal string on the element (id="phone-number"),
  • id={@id} bound to an attr declared as attr :id, :string, required: true or attr :id, :string, default: "<literal>" (binary default), or
  • id={<expr>} where the interpolation references only @<name> assigns that are each themselves declared as a stable-id attr — e.g. id={@id <> "-trigger"} with attr :id, :string, required: true.

The attr name does not have to be literally :id — any name works, as long as every @<name> referenced inside the id={...} interpolation is declared with required: true or a binary default:.

Anything else — no id= on the phx-hook element, a bare @<name> that isn't declared as a stable-id attr, an id={...} expression that references no assigns or references an unstable assign, or an attr with default: nil — is flagged.

The check walks each module, tracks attr declarations preceding each def, and flags any function whose preceding attr block is non-empty and whose ~H template has a phx-hook-carrying element without a stable id binding.

Bad

attr :class, :string, default: nil
def phone_number(assigns) do
  ~H"""
  <div id={@id} phx-hook=".PhoneNumber" class={@class}>...</div>
  """
end

# `default: nil` doesn't guarantee a stable id
attr :id, :string, default: nil
def phone_number(assigns) do
  ~H"""
  <div id={@id} phx-hook=".PhoneNumber">...</div>
  """
end

Good

attr :id, :string, required: true
attr :class, :string, default: nil
def phone_number(assigns) do
  ~H"""
  <div id={@id} phx-hook=".PhoneNumber" class={@class}>...</div>
  """
end

# attr has a binary default — `@clear_button_id` is always populated
attr :clear_button_id, :string, default: "search-clear-button"
def clear_button(assigns) do
  ~H"""
  <button id={@clear_button_id} phx-hook="InputClearButton">...</button>
  """
end

# derived id is fine when every referenced assign is stable
attr :id, :string, required: true
def trigger(assigns) do
  ~H"""
  <button id={@id <> "-trigger"} phx-hook=".Trigger">...</button>
  """
end

# literal id on the element
def phone_number(assigns) do
  ~H"""
  <div id="phone-number" phx-hook=".PhoneNumber">...</div>
  """
end

Check-Specific Parameters

There are no specific parameters for this check.

General Parameters

Like with all checks, general params can be applied.

Parameters can be configured via the .credo.exs config file.