Files
Htmx/docs/Issues/Component-API-Smells.md
T

15 KiB

Component API Smells

This document catalogs potential complaints about the current component/template API.

Each item includes:

  • Problem
  • Why it hurts
  • Potential solutions
  • Consequences/costs

1) Magic Strings for Variants, Sizes, Types, Directions

Problem:

  • Many components use raw strings for semantic options (variant, size, type, direction, align, etc.).
  • Invalid values often silently fall back to defaults.

Why it hurts:

  • No compile-time safety.
  • Typos are easy to miss.
  • Weak IntelliSense discoverability.

Potential solutions:

  • Replace string options with enums for common semantic domains.
  • Generate constants classes per component for non-breaking intermediate step.
  • Add analyzers that validate allowed literals where strings are retained.

Consequences/costs:

  • Enums can be breaking if public signatures change.
  • Constants are low cost but do not fully prevent invalid values.
  • Analyzer route adds tooling complexity.

2) Inconsistent Styling Extensibility (extraClasses and wrappers)

Problem:

  • Some components have extraClasses; others do not.
  • Developers often wrap components in outer div just to apply layout/styling.

Why it hurts:

  • No consistent mental model.
  • Extra wrapper markup increases noise and nesting depth.

Potential solutions:

  • Add a standard className (or extraClasses) parameter to every component.
  • Support class merging utility behavior in a shared helper.

Consequences/costs:

  • Public constructor expansion across many components.
  • Need policy for precedence (base classes first vs custom classes first).

3) Missing Uniform Attribute Pass-Through

Problem:

  • Attribute extensibility is fragmented (hxAttrs in some places, none in others).
  • No first-class support for arbitrary aria-*, data-*, test IDs, analytics attributes.

Why it hurts:

  • Manual string composition is error-prone.
  • Difficult accessibility and testing instrumentation.

Potential solutions:

  • Add a shared attributes bag type (IReadOnlyDictionary<string, string?>).
  • Keep hxAttrs temporarily as compatibility shim.

Consequences/costs:

  • Larger refactor surface.
  • Slight allocation/processing overhead.
  • Requires HTML attribute encoding rules in one central place.

4) hxAttrs Raw String Footgun

Problem:

  • Raw attribute strings allow malformed markup or accidental injection.

Why it hurts:

  • Hard-to-debug render bugs.
  • Security posture depends on each caller doing manual encoding correctly.

Potential solutions:

  • Deprecate raw hxAttrs in favor of typed/structured attrs.
  • Provide safe helper methods to construct HTMX attribute sets.

Consequences/costs:

  • Migration needed for existing call sites.
  • Potentially breaking unless a gradual fallback is kept.

5) Unsafe-by-Default Raw HTML Content Paths

Problem:

  • Several components accept string content that is rendered as raw HTML.
  • Caller must remember to encode user-provided values.

Why it hurts:

  • XSS risk in real application code.
  • Easy to misuse when moving quickly.

Potential solutions:

  • Safe-by-default encoding for plain string inputs.
  • Separate APIs for encoded text vs trusted HTML (explicit escape hatch).
  • Introduce a SafeHtml wrapper type for intentional raw HTML.

Consequences/costs:

  • Safe-by-default may break current behavior for callers relying on raw HTML.
  • Trusted-HTML API adds conceptual complexity, but clearer intent.

6) Inconsistent Security Guidance in Component Docs

Problem:

  • Some docs mention encoding; others do not provide clear warnings.

Why it hurts:

  • Security correctness relies on tribal knowledge.

Potential solutions:

  • Add a standardized "Security" section to every component doc.
  • Include explicit examples: safe input, unsafe input, and fix.

Consequences/costs:

  • Documentation maintenance overhead.
  • Strong DX/security benefit for low implementation cost.

7) No Explicit "Component Props" Model in Markup

Problem:

  • Slots replace placeholders, but there is no direct concept of passing typed props in .htmx markup itself.
  • Dynamic behavior is mostly constructor-centric in .htmx.cs.

Why it hurts:

  • Feels unlike modern component systems where props are explicit and local.
  • New users may expect inline component parameterization and be surprised.

Potential solutions:

  • Document this limitation clearly as a design constraint.
  • Add a generated props record convention per component/page.
  • Explore optional parameterized slot syntax in generator (long-term).

Consequences/costs:

  • Props model requires generator design changes.
  • Parameterized slot syntax is high complexity and may conflict with AOT simplicity.

8) Constructor Bloat and Low Readability

Problem:

  • Some components expose many optional parameters, often multiple strings.

Why it hurts:

  • Ambiguous calls and poor self-documentation.
  • Easy to mis-order arguments.

Potential solutions:

  • Favor required args + options record pattern.
  • Add fluent builders for complex components.

Consequences/costs:

  • Options records improve readability but introduce extra types.
  • Builders can increase allocations and complexity.

9) Tuple-Based APIs for Complex Components

Problem:

  • Components like tabs/accordion/dropdown/table rely on tuple collections.

Why it hurts:

  • Tuples are easy to misuse and harder to read than named objects.
  • Harder to evolve APIs without breaking all call sites.

Potential solutions:

  • Replace tuple parameters with named records (TabItem, AccordionItem, etc.).

Consequences/costs:

  • Migration churn across existing usage.
  • Clear long-term maintainability win.

10) Missing Validation Feedback for Invalid Inputs

Problem:

  • Invalid option values often degrade silently rather than failing fast.

Why it hurts:

  • Bugs are hidden and discovered late.

Potential solutions:

  • Add debug-time validation with clear exceptions/messages.
  • Optionally emit logs/diagnostics in production with safe defaults.

Consequences/costs:

  • Strict runtime validation can be breaking for existing invalid usages.
  • Diagnostics-only mode is safer for migration.

11) Inconsistent Boolean Option Naming

Problem:

  • Different components use varying naming styles for booleans and toggles.

Why it hurts:

  • Low API predictability.

Potential solutions:

  • Define naming conventions (isX, hasX, enableX) and enforce globally.

Consequences/costs:

  • Rename churn if normalized retroactively.

12) CSS Contract Coupled to JS via Hidden Class Names

Problem:

  • Interactive behavior relies on specific class/data selectors that are effectively API contracts.

Why it hurts:

  • Refactoring classes can break behavior.
  • Coupling is not obvious from constructor APIs.

Potential solutions:

  • Document required selectors/events in each interactive component doc.
  • Prefer stable data-component/data-role markers over purely visual class names.

Consequences/costs:

  • Markup updates across components and JS.
  • Better long-term resilience to style refactors.

13) Runtime Behavior Dependencies Not Surfaced in API

Problem:

  • Components requiring JS initialization do not expose that requirement in code signatures.

Why it hurts:

  • Silent "renders but does not work" failures.

Potential solutions:

  • Add Requires JavaScript section in docs and XML comments.
  • Add lightweight marker interface or metadata attribute for interactive components.

Consequences/costs:

  • Minimal runtime cost; mostly documentation/tooling work.

14) Accessibility Ergonomics Gaps

Problem:

  • No consistent way to pass aria-*, id, for, describedby across all components.

Why it hurts:

  • Accessibility quality depends on manual wrapper hacks.

Potential solutions:

  • Introduce shared accessible options type.
  • Provide defaults and enforce required labels where relevant.

Consequences/costs:

  • Constructor changes and additional validation logic.

15) Testability Friction (No Standard Test IDs)

Problem:

  • No consistent data-testid or attribute pass-through strategy.

Why it hurts:

  • E2E selectors become brittle (class/text-based selectors).

Potential solutions:

  • Add standard attributes bag and testing guidance.
  • Add testId convenience parameter in interactive/form primitives.

Consequences/costs:

  • Minor API surface increase.
  • Significant test stability benefit.

16) Documentation Discoverability Gaps

Problem:

  • Component docs focus on usage but under-emphasize known limitations and smell areas.

Why it hurts:

  • New contributors re-learn the same constraints repeatedly.

Potential solutions:

  • Add dedicated docs for limitations, anti-patterns, and migration strategy.
  • Add an index from component reference into Issues docs.

Consequences/costs:

  • Ongoing documentation upkeep.

17) Inconsistent Naming (extraClasses vs alternatives)

Problem:

  • Similar concepts have inconsistent parameter names.

Why it hurts:

  • Context switching overhead.

Potential solutions:

  • Standardize naming dictionary and enforce in reviews.
  • Offer temporary backward-compatible aliases.

Consequences/costs:

  • Alias support increases short-term complexity.

18) Lack of Strongly-Typed Domain Primitives

Problem:

  • IDs, route paths, CSS classes, and labels are all plain strings.

Why it hurts:

  • Accidental parameter swaps and weak intent signaling.

Potential solutions:

  • Introduce lightweight value objects or records for high-value domains (DialogId, CssClassList, etc.).

Consequences/costs:

  • Added type count and conversion code.
  • Better readability and safer APIs.

19) Missing Centralized Class Composition Policy

Problem:

  • Tailwind class strings are composed ad-hoc in constructors.

Why it hurts:

  • Risk of duplicate/conflicting classes.
  • Hard to audit variant behavior consistency.

Potential solutions:

  • Add shared class composition helper utilities.
  • Optionally adopt a deterministic merge utility pattern.

Consequences/costs:

  • New utility dependency or internal helper maintenance.

20) Limited Error Reporting for Misconfigured Interactive Components

Problem:

  • Missing/incorrect JS hooks often fail quietly.

Why it hurts:

  • Time-consuming debugging.

Potential solutions:

  • Development-only console warnings/assertions from components.js when expected markers are missing.

Consequences/costs:

  • Slight JS complexity increase.
  • Better troubleshooting experience.

21) Form Component API Inconsistency

Problem:

  • Form primitives vary in how they accept value/default/checked/attrs/labels.

Why it hurts:

  • Hard to predict usage patterns across components.

Potential solutions:

  • Define a shared form control contract:
    • name, id, label, value, disabled, required, className, attributes

Consequences/costs:

  • Widespread API harmonization work.
  • Major usability win once stabilized.

22) No First-Class Validation/Error State Patterns

Problem:

  • Error display, invalid styling, and message linkage are largely ad-hoc.

Why it hurts:

  • Inconsistent UX and accessibility for validation states.

Potential solutions:

  • Add canonical form-field wrapper component and error semantics.
  • Add helper patterns in docs for mapping server validation to components.

Consequences/costs:

  • Additional abstractions and migration.

23) Table API Lacks Strong Cell/Column Models

Problem:

  • Table inputs as nested strings are simplistic and rigid.

Why it hurts:

  • Hard to represent links, badges, actions, and per-cell semantics safely.

Potential solutions:

  • Introduce column and row models with typed cell renderers.
  • Support text cell vs trusted HTML cell explicit APIs.

Consequences/costs:

  • Significant redesign effort for table API.
  • High payoff for real-world usage.

24) Potential Over-Eager Precomputation in Constructors

Problem:

  • Some components precompute heavy HTML payloads in constructor.

Why it hurts:

  • Allocation spikes for large datasets.
  • Can surprise developers expecting render-time streaming.

Potential solutions:

  • Lazy compute/cache expensive sections.
  • Document performance profile and guardrails per component.

Consequences/costs:

  • Possible complexity in caching invalidation.
  • Better performance transparency.

25) No Compatibility Policy for API Evolution

Problem:

  • No explicit deprecation policy for parameter renames or behavior changes.

Why it hurts:

  • Contributors hesitate to improve APIs due to break risk.

Potential solutions:

  • Define semver/deprecation policy in docs.
  • Use staged migration with obsolete annotations.

Consequences/costs:

  • Process overhead, but critical for long-term maintainability.

26) No Unified Component Design Principles Doc

Problem:

  • Patterns are documented, but not as enforceable design principles.

Why it hurts:

  • New components may diverge in API style.

Potential solutions:

  • Publish a component API style guide with mandatory rules and preferred patterns.

Consequences/costs:

  • Requires reviewer discipline.

27) Internationalization (i18n) Boundaries Not Explicit

Problem:

  • Many labels/content are plain strings without explicit localization guidance.

Why it hurts:

  • Inconsistent localization strategy across pages/components.

Potential solutions:

  • Add docs for localizable boundaries and resource integration patterns.

Consequences/costs:

  • Documentation and integration work.

28) Missing "Known Limitations" Section in Component Reference Entry Point

Problem:

  • The main component reference does not prominently call out systemic limitations.

Why it hurts:

  • Developers discover constraints by trial and error.

Potential solutions:

  • Add up-front limitations and issue tracker links in component reference.

Consequences/costs:

  • Low cost; immediate discoverability gains.

29) API Surface Differs Across Similar Components

Problem:

  • Similar categories (display/form/interactive) do not expose comparable extension points.

Why it hurts:

  • Surprising differences force re-learning per component.

Potential solutions:

  • Define a baseline component contract by category:
    • Display: className, attributes
    • Form: baseline form control props + attrs
    • Interactive: baseline + JS contract notes

Consequences/costs:

  • Requires systematic API audit and staged rollout.

30) Missing Tooling Support for API Misuse

Problem:

  • No analyzers/code fixes for common mistakes (invalid variant, unsafe content, missing encoding).

Why it hurts:

  • Review burden remains manual.

Potential solutions:

  • Introduce Roslyn analyzers for:
    • magic string validation
    • unsafe raw HTML from untrusted sources
    • missing serialization registration patterns

Consequences/costs:

  • Initial tooling investment is medium-high.
  • Scales quality across the codebase after adoption.

Cross-Cutting Improvement Patterns

  1. Standardized base options record:
  • className
  • attributes
  • testId
  • ariaLabel
  1. Strong typing for semantic options:
  • enums/constants/analyzers
  1. Safe content model:
  • text-safe by default, explicit trusted-html escape hatch
  1. Better docs contract:
  • every component doc should include:
    • security notes
    • accessibility notes
    • JS dependency notes (if interactive)
    • extension points
  1. Migration strategy:
  • additive changes first
  • obsolete old params
  • remove deprecated paths in major version bump