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.