--- type: convention title: Phlex Component Design summary: 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. tags: - type/convention - domain/software-design - tool/phlex - tool/rails scope: global last_updated: 2026-07-13 date: 2026-07-13 related: - sandi-metz-code-philosophy - tdd-methodology source: 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 ## Related - [[sandi-metz-code-philosophy]] — the size/responsibility limits Phlex components must obey - [[tdd-methodology]] — the red-green-refactor loop these components are built with