HTL vs JSX — translation guide
Purpose: Map every JSX habit to its HTL equivalent, and flag the habits that must change.
Who this page is for
| Audience | Why it matters to you |
|---|---|
| React engineers | The fastest way to become HTL-fluent |
Direct translations
| You write in JSX | You write in HTL |
|---|---|
{title} | ${model.title} |
{cond && <p>…</p>} | <p data-sly-test="${model.cond}">…</p> |
{items.map(i => <li key>…</li>)} | <li data-sly-list.i="${model.items}">…</li> |
<Child {...props} /> | <sly data-sly-resource="${'child' @ resourceType='…'}"></sly> |
| Reusable partial component | data-sly-template + data-sly-call |
className={clsx(…)} | Ternaries in class attr, or map via data-sly-attribute |
Fragment <>…</> | <sly>…</sly> |
Habits that must change
| React habit | Why it doesn't transfer | Instead |
|---|---|---|
Logic in the template (.filter().sort() inline) | HTL forbids arbitrary expressions | Do it in the Sling Model |
| Event handlers in markup | HTL emits static HTML | Separate JS in clientlibs, bind by class/data-attr |
| Component state | No render-time state; one render per request | State is content (JCR) or client-side JS |
| Conditional early returns | Templates are declarative only | data-sly-test regions |
| Runtime data fetching in component | Server render is synchronous over content | Model fetches (sparingly); client JS for dynamic data |
The mental switch
JSX components own behaviour and markup; HTL owns markup only. The triad is: Sling Model (logic) + HTL (markup) + clientlib JS (behaviour). It is the same separation you'd get extracting all logic from JSX into hooks and all interactivity into event modules — enforced by the platform.
Worked example
The PHI benefits list in React: a useBenefits() hook filters covered items, JSX maps them, onClick toggles detail. In AEM: PlanCardModel.getBenefits() returns view-ready rows; HTL lists them; plancard.js in the clientlib binds the toggle to .plan-card__row. Three files, one responsibility each.