The New PHP

When PHP launched, mixing HTML and code in one file seemed obvious. Why keep them apart? The markup sits right there. So does the data.

Then came <div> soup.

More accurately: you couldn’t look at a file and tell which lines decided what to show from the ones deciding how it looked. We spent 20 years digging out. Then we walked straight back in, with better tooling.

Loops

returning markup from a function can make sense. It stops making sense when the only way to repeat something means calling a method on an array.

<ul>
	{getPhoneNumbers().map((phoneNumber) => (
		<li key={phoneNumber}>{phoneNumber}</li>
	))}
</ul>

That arrow should alarm you. If you don’t write javascript every day, or if you came up in an MVC framework like Rails or Laravel, that line reads as neither markup nor code.

Look at what you already have to know to get through 4 lines. Arrays have a .map. It takes a callback. An arrow function returns its expression without return. Those brackets after the => group rather than call. And key belongs to React, not to HTML.

Svelte:

<ul>
	{#each getPhoneNumbers() as phoneNumber}
		<li>{phoneNumber}</li>
	{/each}
</ul>

Nothing to know. It says “each”. A designer can edit it.

Astro sits in the middle, and I build with Astro, so let me give it credit. It still reaches for .map in the template. But the logic goes above the markup, in a fenced block that runs on the server and never ships:

---
const PHONE_NUMBERS = ["+1-825-252-5073", "+1-825-252-5074"];
---

<ol>
	{PHONE_NUMBERS.map(function toItem(phoneNumber) {
		return <li>{phoneNumber}</li>;
	})}
</ol>

Computing here, presenting there. That recovers most of what got lost. Still no each, though.

className

Then the vocabulary.

class counts as a reserved word in javascript, so JSX made you write className. Same story with htmlFor. Same story with every event handler getting camel-cased.

Sounds like nitpicking. It costs you 15 years of accumulated HTML knowledge: MDN, the spec, every Stack Overflow answer you will ever read, all of it sitting behind a find-and-replace. The thing in your editor does not match the thing in the docs.

Giving Ground

Co-location does not deserve the blame.

If a component holds a bit of state only it touches, keeping that beside its markup helps. Svelte does it. Vue does it. Astro does it. Deleting one file to remove a feature buys you a lot.

It goes wrong at scale. In an enterprise codebase with acceptance criteria hanging off it, the logic deciding whether a user may see a thing does not belong to a component. Call it a business rule, and business rules belong somewhere you can test them without rendering anything.

So: anything testable without a DOM should live somewhere with no DOM. A component may use that logic. It should not hold it.

And you can do that in JSX. Nothing forbids it. That frustrates me most, because nothing encourages it either, and every example in the ecosystem puts the fetch, the transform and the markup inside one function.

The Real Problem

JSX works as a template language and refuses to admit it.

No if, so you reach for && and one day a bare 0 shows up on screen because your array ran empty. No each, so you reach for .map and remember key. No class, so you learn a second word for it. No way to mark “this holds markup” and “this holds logic”, so it says everything in javascript and leaves you to sort it out by eye.

Pug solved these. ERB solved these. Svelte solved these. Astro mostly solved these. Every one of those weighs less than the JSX transform, and every one of them can go to somebody who does not write javascript.

I use JSX. I will keep using it, because that pays and that ships. But let me stop calling the ergonomics a feature.