DEV Community


HTML over the wire with Preact

schniz profile image Gal Schlezinger Originally published at on ・9 min read

I’m using Rails on a side project I’m playing with. Many of my peers would probably ask why would I do this to myself. The answer is simple: Rails helps me get stuff done quickly because it is super boring. It is so boring that it makes me excited.

My app is split in two: a widget that every website can use — a JS bundle, and a back-office/API. For the back-office I mainly use Rails and the magnificent Alpine.js. Authoring server-side rendered routes is so much easier to do with these two. Rails provides all the things I need in terms of back-end (even E-mailing comes built in!), and Alpine allows me to sprinkle JS as if my HTML was a React application: declarative, co-located JavaScript. For the widget, I use Preact. I originally started it as a React project, but I wanted to keep a minimal bundle size.

I launched a new project, and I immediately installed graphql-ruby as a GraphQL server implementation, to easily declare resources that can later be translated into type-safe data fetching from my widget. I mostly do TypeScript so it’s soothing me knowing that I can generate types and enforce them at runtime. I used urql as a GraphQL client, because it looked like it would result in a smaller bundle (~4 times smaller than Apollo) and I wanted to experiment with it.

By measuring the bundle size using tools like Webpack Visualizer, I found out that Urql bundles graphql.js to the client, and that’s something that I don’t really need — therefore, I do not want. Turned out, that Urql and its dependencies were more than 50% of my bundle size. I mean, this wasn’t very large and I was quite satisfied with Urql, but this is a widget, not an entire application. The smaller - the better - and I want GraphQL for the amazing developer experience coming from the tight TypeScript integration, but that’s something I’m fine with sacrificing in favor of my production bundle size (or solve later). Therefore, I decided to drop GraphQL and migrate my data fetching to use simple REST endpoints, with swr to hook up with Preact.

As I started building a landing page, I wanted to make an animation to showcase the product — so I made one by myself with Tailwind CSS and Alpine. Eventually, I had a very clean animation with better looks than the current product. However, since my widget is a Preact app and my server is a Rails app, I couldn’t share the components between my backend and the widget.

Or could I..?

Most Preact and React apps use JSON to pass data between client and server. What if the server already knows how to render stuff? Well, instead of serving JSONs we can serve HTML — Exactly what DHH was preaching about lately when they introduced Hotwire. So, instead of the following payload:

  "message_id": "abcd1234",
  "text": "Hey, friend!",
  "author": {
    "name": "Chandler Bing",
    "avatar_url": ""
Enter fullscreen mode Exit fullscreen mode

I could return the following HTML:

<div id="message-abcd1234">
  <img class="avatar" src="" />
  <div>Hey, friend!</div>
  <span>— Chandler Bing</span>
Enter fullscreen mode Exit fullscreen mode

And use dangerouslySetInnerHTML in Preact and React to show the message. Since I’m using Rails and I know for sure that my HTML is sanitized, that’s not done dangerously at all. This way, I can keep my authorization and render specific layout for specific layouts and keep all his logic in my precious, well-tested back-end.

The funny thing is, that it is not a new thing. The web did that before React was a thing! You don’t have to use JSON! But, since React and other SPA frameworks have taken the world by storm, I regularly meet people who don’t know about old-school frameworks like Rails and Django. And sometimes, the greatest solutions come from mixing modern and old solutions.

Now, this path is not all gummy bears. If you’re into optimistic updates, that’s not the path for you — because it relies on the fact you want to keep as much of the business in your back-end. Rendering HTML is the cherry on top of everything.

Personally, I think that most apps are either offline-centric or online-centric. Being somewhere in the middle is confusing. If you want to implement optimistic updates, you’re probably trying to do that by manually crafting an optimistic response. That can be very hard to maintain, and you can probably get better results if you architecture your app to work offline with tools like PouchDB.

When working on my side-project, I don’t want to waste time on optimistic updates. If my server is down, I’d rather get an error. I want my project to be as simple as possible. It’s not a realtime chat application.

It’s also harder to bind to event handlers, compared to Preact-rendered applications. How would you “rehydrate” the HTML coming from the server? How can you ensure the buttons whatever you need when they are being clicked? Consider the following HTML:

<button onclick="what_should_this_fn_be()">Click me!</button>
Enter fullscreen mode Exit fullscreen mode

what_should_this_fn_be() needs to be replaced with something in order for our button to be interactive. It can be inline JS, like the good ol’ days, but we won’t be able to bind it to functions in our bundle if we’re minifying them — or we would have to export them globally. Anyway, this ship has sailed. We need a better solution for event binding in our dynamic HTML sections:

Using event bubbling

This is the “manual” or “explicit” way. It’s been in use for years.

When adding onClick={myFunction} in Preact and React, you will actually get events that bubbled from the children of the provided DOM node — not just events that happened on the specific DOM node. This is a great way to solve our issue — if you have dynamic HTML that can be clicked, you can lift the event handling to the container, which lives in Preact and renders the dynamic HTML. So instead of having just a <button>, you can add some hints like <button data-action="doSomething">, and reference this data-action in your event handler:

function MyComponent() {
  const html = `<button data-action="showAnAlert">click me</button>`;
  return (
      dangerouslySetInnerHTML={{ __html: html }}
      onClick={(event) => {
        if ( === "showAnAlert") {
          alert(`Look at me, I'm doing something!`);
Enter fullscreen mode Exit fullscreen mode

This way, the server can declaratively say what is the role of a button, and you can have the implementation in JS land.

Using custom elements

We can expose Preact elements as custom elements. So, instead of having the following code:

<button>What should I do?</button>
Enter fullscreen mode Exit fullscreen mode

We can use a custom component:

<my-alert-button>Show an alert!</my-alert-button>
Enter fullscreen mode Exit fullscreen mode

That would work kinda well with Preact, and can also be reused in our Rails backend. In fact, that’s what I do when rendering icons inside the Rails and the widget app, as I mentioned on this one tweet. That is somewhat a win, but when used heavily, it creates some issues.

First, I will have to work with Shadow DOM and will go outside of Preact land just to go back using Preact using the custom element. So Preact -> HTML -> Custom Element -> Preact. I can live with it but there is a better solution, that doesn’t have that massive accessibility issue:

dangerouslySetInnerHTML hurts accessibility

The big issue for both the solutions mentioned before is the accessibility issue coming from dangerouslySetInnerHTML: when the HTML is replaced, the DOM elements will be replaced by detaching them from the DOM and attaching new elements. That means that you lose focus and DOM state — So if you had input fields or details popovers, they will be reset.

When using a library that does DOM diffing for you, doesn’t matter if it’s virtual or not, you want to use this diff. So in Preact, we would probably want to parse our HTML into Preact elements, so Preact will know how to diff them. In React, we would want to make them React elements. In Svelte, I’m pretty sure we wouldn’t have any way of doing so because all the diffing is compiled away — so we would need to use a library like morphdom to do that.

Let’s talk about Preact.

Using preact-markup

Preact Markup is a cool project that parses HTML to Preact elements, allowing you to render custom HTML elements using Preact components, without the real component boundary. It even lets you override standard HTML elements with your own components. Check out the following example, which has a my-button element and overriding the standard button one:

Preact Markup’s implementation is rather easy to understand. I suggest you to try building one yourself to fully grasp the ideas there. It can be translated to React very easily. Maybe that could be a future blog post, who knows?

Summing up

Getting HTML back from the server and injecting that to our client-side apps is so nice. It works tremendously with SWR, and helped me building my side-project in a veeeeeery fast pace. The Server Components initiative by the React team is probably onto something — but you don’t need React to get the server magic. It’s all a question of trade-offs. If server-side rendering is mostly your jam, you could stick with it.

Once you need a more complicated behaviors, you can always make a JSON response — and maybe you will find yourself embedding a server-generated HTML into it to sweeten the pill 😉

Discussion (0)

Forem Open with the Forem app