---
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/conventions/phlex.md
---

# 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
