I’ve worked on so many projects recently that were more complicated than they needed to be because they used JavaScript to generate HTML.
JavaScript is…
- Slower to load
- Slower to run
- More prone to breaking
- Harder to read and reason about
- Doesn’t actually look like the final output
It’s inferior to just using HTML in nearly every way.
I’m not saying never use JavaScript, though. I think JS is great at augmenting and enhancing what’s already there, and adding interactivity that cannot (yet) but handled with HTML.
Let’s look at two examples…
I see this a lot in React and JSX.
Every input in a form has an input listener on it. Any changes to that input update a state property. That property is used to set the value of the input, creating this weird circular logic.
(This approach is called “controlled inputs” in React-land, and some devs are slowly moving away from it, finally.)
The form submit is often also tied to clicking a <button> rather than submitting a form, meaning that hitting enter on an input won’t submit the form. This removes a native accessibility feature.
function Login () {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
function handleSubmit () {
if (!username || !password) {
// Show error message
return;
}
fetch('/login', {
method: 'POST',
body: JSON.stringify({
username,
password
}),
});
}
return (
<form onSubmit={event => event.preventDefault()}>
<label for="username">Username</label>
<input
id="username"
type="text"
onInput={event => setUsername(event.value)}
value={username}
/>
<label for="password">Password</label>
<input
id="password"
type="password"
onInput={event => setPassword(event.value)}
value={password}
/>
<button onClick={handleSubmit}>Submit</button>
</form>
);
}
Here’s that same setup with HTML…
<form action="/login" method="POST">
<label for="username">Username</label>
<input
id="username"
type="text"
required
/>
<label for="password">Password</label>
<input
id="password"
type="password"
required
/>
<button>Submit</button>
</form>
And then you can enhance it with just a touch of JavaScript…
const form = document.querySelector('[action*="/login"]');
form.addEventListener('submit', event => {
event.preventDefault();
const data = new FormData(form);
const body = JSON.stringify(Object.fromEntries(data));
fetch('/login', {
method: 'POST',
body
});
});
Hell, you can even do this in React if you want!
function Login () {
function handleSubmit (event) {
event.preventDefault();
const data = new FormData(event.target);
const body = JSON.stringify(Object.fromEntries(data));
fetch('/login', {
method: 'POST',
body
});
}
return (
<form onSubmit={handleSubmit}>
<label for="username">Username</label>
<input
id="username"
type="text"
required
/>
<label for="password">Password</label>
<input
id="password"
type="password"
required
/>
<button>Submit</button>
</form>
);
}
API responses
Another area where you can lean a lot more heavily on HTML is API responses.
Let’s say you have a <table> that gets generated based on some data that’s specific to the user or some selections that have been made in a <form>.
In most modern apps, that means getting back some JSON, and generating a <table> from it.
const app = document.querySelector('#app');
const request = await fetch('/my-wizards');
const response = await request.json();
app.innerHTML =
`<table>
<thead>
<tr>
<th>Name</th>
<th>Location</th>
<th>Powers</th>
</tr>
</thead>
<tbody>
${response.wizards.map(wizard => {
const {name, location, powers} = wizard;
const row =
`<tr>
<td>${name}</td>
<td>${location}</td>
<td>${powers}</td>
</tr>`;
return row;
}).join('')}
</tbody>
</table>`;
But if a server has to do the work of getting that information and sending it back to you anyways, it could also just send the <table> HTML, which you could then render into the UI.
const app = document.querySelector('#app');
const request = await fetch('/my-wizards');
const response = await request.text();
app.innerHTML = response;
There are workflow changes
This, of course, changes the workflow of building apps quite a bit.
A lot of work shifts to the backend that in today’s apps is handled with client-side code. But… that’s a good thing?
It means faster, simpler apps that behave more predictably and reliably.