Haskellpreneur

Unbundled React/Preact in a multi-page app

Get the power of React with no installation in frameworks like IHP, Laravel, Rails and Phoenix.

The single-page app (SPA) has taken over the modern web with advantages and drawbacks.

I don't think I will write a pure SPAs again any time soon. I wish to access the web platform directly, and utilize more advanced tools when needed.

The multi-page apps (MPA) with their HTML-first approach are gaining momentum again with good reason.

With MPAs (PHP, MVC frameworks), you get quick page loads, greater simplicity and less flaky Node.js dependencies.

But you might occationally still want the client-side state that React, Svelte, Elm or Vue provide.

Islands archtecture

This is where the islands architecture is a good solution. Just create small apps/widgets for certain parts of the page.

The drawback is often that you need a bundling step to get it working.

One of the lowest barriers to that goal might be Preact (or React) and htm in a vanilla JS file. No bundler, no node.js, no transpiling/compiling.

You even get the hooks API!

Preact with hooks and a lightweight alternative to JSX

Preact market themselves as a "3kB alternative to react with the same modern API". It can pretty much give you all you need from React, but with lower baggage.

If we start with an index.html HTML file, the entrypoint we need is highlighted below:

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Quick Preact with Hooks</title>

</head>

<body>
<div class="counter" data-initial-value="1"></div>
<script src="./counter.module.js" type="module"></script>
</body>

</html>

The <div> element with class="counter" will be the node that the counter.module.js file will attach itself to.

Note that I am setting the initial values for this component via data-attributes directly on the<div> element.

Setting initial values via your templating engine should be effortless through data attributes. It also saves you from writing HTTP requests.

Also note type=module in the script tag. This means that the js file is a JavaScript module.

Skypack instead of npm

Skypack gives you access to npm libraries pre-bundled.

In a JavaScript module, we can import libraries directly from Skypack.

These are the imports we will use throughout this tutorial.

import { useState, useEffect } from "https://cdn.skypack.dev/preact@10/hooks";
import { render, html } from "https://cdn.skypack.dev/htm@3/preact";

The import syntax is available now in vanilla JavaScript and supported by all modern browsers!

As you see in the paths, you are free to define version like preact@10 or even preact@^10.5.0 for protection from breaking changes.

htm instead of JSX

htm, or Hyperscript Tagged Markup, is a way of writing views in JSX-like syntax without any transpiling or compiling.

You can do the same with React, but I will use Preact since it's mostly a better fit for this use-case.

Let's create the counter.module.js file the HTML file we made above expects. First without any state logic:

import { render, html } from "https://cdn.skypack.dev/htm@3/preact";

function Counter({ initialValue }) {
return html`
<h1>Counter</h1>
<div>
${initialValue}</div>
<button>Increase</button>
<button>Decrease</button>
`
;
}

const appElement = document.querySelector(".counter");

render(
html`<${Counter} initialValue=${appElement.dataset.initialValue} />`,
appElement
);

The html function uses template literals. This enables us to interpolate javascript like this: ${} in place of the JSX brackets {}.

Other than that, it's almost identical to how it works with React and JSX!

Rendering many of the same widget

When working with a multi-page app, you might also want to initialize the same widget many places.

You can improve the render logic by iterating over every element with the given class. This is why I use class over id.

const appElement = document.querySelector(".counter");

render(
html`<${Counter} initialValue=${appElement.dataset.initialValue} />`,
appElement
);

const appElements = document.querySelectorAll(".counter");

appElements.forEach((appElement) => {
render(
html`<${Counter} initialValue=${appElement.dataset.initialValue} />`,
appElement
);
});

This way, you can add unlimited widgets on the same page, and initialize with different values like this:

    <div class="counter" data-initial-value="1"></div>
<div class="counter" data-initial-value="10"></div>
<script src="./counter.module.js" type="module"></script>

Each widget will have their own state without being dependent on each other. They are initialized with their respective initial values.

Two Preact components acting independently

Making components

Splitting the widget into smaller components is easy. Let's make a very trivial example by creating a reusable component out of the h1 element.

function Title({ title }) {
return html` <h1>${title}</h1> `;
}

function Counter({ initialValue }) {
return html`
<
${Title} title="Counter" />
// ...etc
`
;
}

Very simple!

Adding hooks

The hooks API is available from the preact library by importing them from preact/hooks. This gives you the hooks API with all it's benefits.

import { useState, useEffect } from "https://cdn.skypack.dev/preact@10/hooks";
import { render, html } from "https://cdn.skypack.dev/htm@3/preact";

function Title(props) {
const { title } = props;
return html` <h1>${title}</h1> `;
}

function Counter(props) {
const { initialValue } = props;
const [count, setCount] = useState(Number(initialValue));

const increaseCount = () => setCount(count + 1);
const decreaseCount = () => setCount(count - 1);

useEffect(() => {
console.log(`Count changed to ${count}`);
}, [count]);

return html`
<
${Title} title="Counter" />
<div>
${count}</div>
<button onclick=
${increaseCount}>Increase</button>
<button onclick=
${decreaseCount}>Decrease</button>
`
;
}

const appElements = document.querySelectorAll(".counter");

appElements.forEach((appElement) => {
render(
html`<${Counter} initialValue=${appElement.dataset.initialValue} />`,
appElement
);
});

Custom hooks

It can often be a good idea to organize a collection of hooks into one custom hook. It's a great way of separating the view logic from state and actions.

import { useState, useEffect } from "https://cdn.skypack.dev/preact/hooks";
import { render, html } from "https://cdn.skypack.dev/htm/preact";


function Title(props) {
const { title } = props;
return html` <h1>${title}</h1> `;
}


// The custom hook
function useCounter(initialValue) {
const [count, setCount] = useState(Number(initialValue));

const increaseCount = () => setCount(count + 1);
const decreaseCount = () => setCount(count - 1);

useEffect(() => {
console.log(`Count changed to ${count}`);
}, [count]);

return { increaseCount, decreaseCount, count };
}


function Counter(props) {
const { initialValue } = props;
// Notice that the hooks from previous code snippet has been moved into the `userCounter` hook.
const { increaseCount, decreaseCount, count } = useCounter(initialValue);

return html`
<
${Title} title="Counter" />
<div>
${count}</div>
<button onclick=
${increaseCount}>Increase</button>
<button onclick=
${decreaseCount}>Decrease</button>
`
;
}

const appElements = document.querySelectorAll(".counter");

appElements.forEach((appElement) => {
render(
html`<${Counter} initialValue=${appElement.dataset.initialValue} />`,
appElement
);
});

Full code

As you can see, very little code and setup required to get interactive widgets inside your MVC framework, or PHP app if you prefer that:

Benefits

Drawbacks

What if I prefer to use React?

I guess the hero of this story is the htm library. It's also a great match for React:

import ReactDOM from "https://cdn.skypack.dev/react-dom";
import { html } from "https://cdn.skypack.dev/htm/react";

function App() {
return html`<a href="/">Hello!</a>`;
}

ReactDOM.render(html`<${App} />`, document.getElementById("counter"));

This gives you access React libraries, and you can import them from Skypack!

I couldn't figure out how to combine htm with preact/compat, a Preact drop-in replacement for React. Please reach out to me if you figure it out 🙂

My conclusion

Preact modules are practical for quick prototyping of interactivity in a framework like IHP.

Ultimately, I want to have trustable code that won't break. I will continue using Elm, especially when a module becomes large or when the codebase matures in general.

Preact + htm can be great for the extra initial speed when building a minimum viable product.