SecondBrain/convention/phlex-component-design.md

2.9 KiB

type title summary tags scope last_updated date related source
convention Phlex Component Design Rules for building Rails UI components in Phlex (pure-Ruby templating) instead of ERB/ViewComponent — when to use it, how to structure and test a component, and the common lifecycle-hook mistakes.
type/convention
domain/software-design
tool/phlex
tool/rails
global 2026-07-13 2026-07-13
sandi-metz-code-philosophy
tdd-methodology
hyperthrive_dev

Phlex Component Design

Purpose

Governs when to reach for Phlex over ViewComponent/ERB, and how to structure a Phlex component so it stays testable and safe by default.

Core Principles

1. Every component is a plain Ruby class with no template file. Phlex trades template-language flexibility for structural safety (XSS protection by default) and full unit-testability — treat a component like any other Ruby object with explicit dependencies.

2. Explicit dependencies, injected via initialize. Pass all data a component needs through its constructor rather than reaching into globals or helpers. This is what makes a component testable in isolation.

3. Sandi Metz limits apply. Classes < 100 lines, methods < 5 lines, initialize params < 4 — a component doing more than one UI concern should be split. See sandi-metz-code-philosophy.

4. Coverage floor: ≥ 90%, covering init/render/conditional-branch/edge cases. A component's tests are the spec for its behavior; untested branches are unverified behavior in production markup.

Patterns

Choosing Phlex vs ViewComponent. Use Phlex for anything new: components with slots/composition, or anything needing unit tests. Keep ViewComponent only for legacy .html.erb templates already in that form; don't introduce it for new work. One-off, never-reused markup doesn't need a component at all — inline it and extract only once duplicated.

Test shape. Cover four categories per component: initialization (params, defaults, edge cases), rendering (HTML structure/classes/text), conditional logic (every branch), and edge cases (nil/empty/boundary values).

Anti-Patterns

  • Defining template instead of view_template → wrong lifecycle hook name, silently no-ops
  • include ActionView::Helpers::* directly → use the Phlex::Rails::Helpers::* adapters instead
  • Overriding a lifecycle hook (e.g. before_render) without calling super → breaks the parent's setup silently
  • Building markup inside initialize → markup belongs only in view_template; initialize is for storing dependencies
  • render raw(user_input) → reintroduces XSS; wrap trusted HTML in safe() instead, never wrap untrusted input