Skip to main content Accessibility Feedback

A mostly vanilla JS way to use Preact

Yesterday, we learned about DOM diffing and data reactivity, and used my state-based UI library Reef as a teaching tool.

You may be onboard with this approach, but convincing your client or the rest of your team to ditch their beloved React or Vue for some tiny open source project is typically a hard sell.

Today, I’m going to show you how get a similar authoring experience using Preact, a 3kb alternative to React with the same API and some nice features React doesn’t have.

Let’s dig in!

Why Preact is better than React

Preact is better than React for a handful of reasons.

  • Preact is 3-4x faster the start working, and renders UI updates 3x faster, too.
  • Preact lets you write your templates with template literals instead of JSX and a compiler.
  • Preact provides a way to manage state as a standalone object (React forces you to keep state in components).

Did I mention it’s smaller, faster, and better? It is!

Loading Preact

For our purposes, we need three Preact methods: html(), render(), and signal().

The render() method does DOM diffing. The html() method converts our template literals into an object of elements that render() uses under-the-hood to render things (it’s like JSX for the browser). And the signal() method creates a reactive data object, just like store() does in Reef.

While the Preact docs provide a load from CDN option as their default, the signals() method is not part of that documentation.

After some digging, I found a CDN that includes all of the methods we need. It uses ES modules, so we’re going to have to add [type="module"] to our script element and import our methods.

<script type="module">
	import {html, render, signal} from 'https://cdn.jsdelivr.net/npm/preact-htm-signals-standalone/dist/standalone.js';
</script>

Creating reactive data

To make our data reactive, we need to pass it into the signal() method.

// Reactive todo data
let todos = signal(JSON.parse(localStorage.getItem('todos-preact')) || []);

You can then access the data object using the value property, like this.

// Get the todo array
let arr = todos.value;

Unlike Reef (and Vue) which react whenever any property in the array or object is changed, Preact signals only react when the main array or object (or primitive value) itself is changed.

That means we have to do kind of clunky stuff like reassign the entire todos.value property whenever we change one of the items in our array.

The easiest way to do that is with array destructuring.

// addTodo()
todos.value = [...todos.value, form.todo.value];

// removeTodo()
let todosTemp = [...todos.value];
todosTemp.splice(index, 1);
todos.value = todosTemp;

Tomorrow, I’ll show you a trick for making that a bit easier to work with.

Using the html() method for our templates

In the getHTML() template function, we need to add the html() method that Preact requires to use template literals instead of JSX.

The html() method uses tagged templates, which means we omit the parentheses (()) when running it.

/**
 * Create the HTML based on the app state
 */
function getHTML () {

	// If there are no todos, show a message
	if (!todos.value.length) {
		return html`<p><em>You don't have any todos yet.</em></p>`;
	}

	// Otherwise, render the todo items
	return html`
		<ul>
			${todos.value.map(function (todo, index) {
				return html`<li>${todo} <button data-delete="${index}">Delete</button></li>`;
			})}
		</ul>`;

}

One little gotcha with the html() method is that we need to run it anytime we’re creating a new template literal. That includes inside the Array.prototype.map() method’s callback function.

We also need to omit the Array.prototype.join() method. The html() method handles converting everything into strings under-the-hood.

Rendering our template

Next, we’ll pass our template and the element to render it to into the render() function as arguments (this is the opposite order of the Reef component() method).

// Create a component
// Renders into the UI, and updates whenever the data changes
render(html`<${getHTML} />`, app);

Preact wants the template you’re passing in to be an element, so we need to wrap it in angled brackets with a self-closing slash (</>). And because it’s a template literal, we need to also pass it into the html() method.

Yes, this is a weird quirk of Preact. And yes, I find it silly.

Running code when our state changes

With Reef, we can listen for reef:render events and run code in response (like saving our todos to localStorage).

In Preact, we can do the same thing with the effect() function. Let’s first add that to our import.

import {html, render, signal, effect} from 'https://cdn.jsdelivr.net/npm/preact-htm-signals-standalone/dist/standalone.js';

Then, we’ll pass in a callback function that saves our items whenever our data is updated.

// Save todos whenever state is updated
effect(function () {
	localStorage.setItem('todos-preact', JSON.stringify(todos.value));
});

Here’s a working demo.

Quirks of Preact

As far as “big and well known” libraries go, Preact is probably one of my favorite.

But to be honest, I still find Reef easier to work. It just works like vanilla JS, without the fuss.

By comparison, Preact…

  • Requires every template literal to get passed into the html() method
  • Requires elements without a closing tag to use a self-closing slash, even if the normal HTML element doesn’t require one (ex. <input> will error, but <input /> will not)
  • Signals force you to set the main object and use destructuring instead of listening for property changes
  • Can’t be loaded is a global object unless you compile it into an IIFE with your own build step

None of these are deal-breakers for me, but they are examples of how tools often introduce new problems when solving existing ones.

Tomorrow, I’ll show you a trick for more easily managing state with Preact.