Back in May (!!), I mentioned why I wanted to use Preact in legacy client stacks, and promised I'd follow-up with some instructions on how. These are those stories.
(I'll probably iterate on these posts a good deal over the coming months before publishing elsewhere. That said, though this page might change, the main gist and topic should stay the same.)
Add p/React to any legacy client codebase
I've mentioned I'm working on a project that has a lot of legacy code. "How legacy?" you ask? Well, let's just say this year I used Response.Write
while refactoring an .aspx page to make it better (mostly so we could pull complicated string logic out of the template and into an include file on our way to removing aspx entirely), something I hadn't done in 20 years.
Note that even 20 years ago Response.Write
wasn't new! Response.Write
was on its way out then!
My task, then: Find a way to write modern code for new features that can live side-by-side with a 20+ year-old build process.
That means no npm, no builds, no transpilation. Any overhead could cause the teams to pull up short.
Luckily, as we've discussed before, I found Preact and HTM and I'm off to the races.
Let's see how to use "React with hooks without transpilation," or, in our case, specifically, "Let's party with React hooks using JavaScript like it's 1999!!! Well, kinda.
WARNING: This doesn't work with Internet Explorer b/c of, at the very least, its dependency on template strings.
Importing the libraries
[P]Reacting like it's 1999
Let's make a normal index.html page to serve with any old web browser, even Apache.
<html>
<head>
<title>Hello, Preact World!</title>
</head>
<body>
<h1>Hello, Preact World!</h1>
</body>
</html>
Now let's include the Preact and HTM libraries so we can do Preact things.
In modern JavaScripting...
Usually -- even in the Preact "no tools" tutorial -- Preact examples online include the Preact library like this:
import { h, Component, render } from 'https://unpkg.com/preact?module';
In modern javascript, we'd have a package.json file for our local node package requirements that would include both Preact and HTM listed as requirements. The first thing we'd do is to "install the package", which would have a package manager read package.json, see those dependencies, & download the HTM and Preact libraries locally automatically. When we finally build our site, any required JavaScript files or libraries like this would automagically be put into the right place.
For this magic to work, each library included has to register its name on the npmjs site, so we what's in our package.json list
maps to the libraries we want.
Libraries in 1999 JavaScripting...
But let's keep coding like it's nineteen ninety-nine and include scripts the old fashioned way, which is to say, "with script
tags".
We want the equivalent of...
<script src="./preact.js"></script>
<script src="./htm.js"></script>
But where can we find those files?
Libraries like HTM and Preact are usually automatically packaged into a number of consumable formats, and the one we'll look for is called the Universal Module Definition.
As this blog post from davidbcalhoun.com remarks:
[UMD] is admittedly ugly, but is both AMD and CommonJS compatible, as well as supporting the old-style โglobalโ variable definition:
(function (root, factory) {
if (typeof define === "function" && define.amd) {
// AMD
define(["jquery"], factory);
} else if (typeof exports === "object") {
// Node, CommonJS-like
module.exports = factory(require("jquery"));
} else {
// Browser globals (root is window)
root.returnExports = factory(root.jQuery);
}
})(this, function ($) {
// methods
function myFunc() {}
// exposed public method
return myFunc;
});
[emphasis mine -mfn]
If we import libraries in UMD format, our 1999 setup won't have define.amd
or exports
in scope, so we'll have each library's exports pushed into our global scope -- window
.
We can get umd versions from any number of CDN sites, which use a pattern based on the unique library names registered on npmjs.com we discussed earlier, and we then add the versions we want to retrieve in the URLs.
Currently, those are:
Let's look at the start of preact.umd.js.
!function(n,l){"object"==typeof exports&&"undefined"!=typeof module?l(exports):"function"==typeof define&&define.amd?define(["exports"],l):l((n||self).preact={})}(this,function(n){ // ...
Okay, that's kind of a mess. Let's prettier it a bit and then look again.
!(function (n, l) {
'object' == typeof exports && 'undefined' != typeof module
? l(exports)
: 'function' == typeof define && define.amd
? define(['exports'], l)
: l(((n || self).preact = {}));
})(this, function (n) {
// ...
Well, whaddya know, that's exactly the UMD format. And, if the file is placed in the root of your website, this
will be window
, so window.preact
will be spun up when the UMD initialization is done. Awesome.
We have two choices for how to include these libraries:
- Download a copy of each locally.
- Grab from the CDNs.
If I'm partying like it's 1999, I'm always doing the first, downloading and serving locally. But for the sake of getting code you can cut, paste, and run more easily with our examples, we'll use #2 for now.
Our index.html
file now looks like...
<html>
<head>
<title>Hello, Preact World!</title>
<script>
// Just to prove what's in `this` in our UMD code is window
console.log(this, this === window);
// Showing that the libraries AREN'T in scope; they're falsy
console.log(!!window.preact, !!window.htm);
</script>
<script src="https://unpkg.com/preact@10.11.3/dist/preact.umd.js"></script>
<script src="https://unpkg.com/htm@3.1.1/preact/standalone.umd.js"></script>
<script>
// Now let's prove we have the libraries in scope.
console.log(!!window.preact, !!window.htm);
</script>
</head>
<body>
<h1>Hello, Preact World!</h1>
</body>
</html>
Exposing render
and html
We have our libraries now, but there is a catch. What we really want to use most of the time are Preact's render
and HTM's html
functions. We don't want to have to say window.preact.render
and window.htm.html
each time we use them, so let's cheat and push those commonly used functions into the global scope the same way an import would have.
We're losing some utility over modern JavaScript practices b/c these will pollute the global scope everywhere, not limit then to where they're imported, etc, but, if we're reasonably careful, that's no big deal.
<html>
<head>
<title>Hello, Preact World!</title>
<script src="https://unpkg.com/preact@10.11.3/dist/preact.umd.js"></script>
<script src="https://unpkg.com/htm@3.1.1/preact/standalone.umd.js"></script>
<script>
window.render = window.preact.render;
window.html = window.htm.html;
</script>
</head>
<body>
<h1>Hello, Preact World!</h1>
</body>
</html>
Okay, we're actually already ready to start writing some Preact code. Let's do it.
Our first component
If you've never created components in a templating system before, you're in for a treat. What we're doing is, essentially, creating custom html tags that we can use in our markup. Let's create a near simplest-case component called MyComponent
.
We could put the component's code inline in index.html in a script
block, but let's create a new file for it and keep all the component code in a new folder. I'm going to create MyComponent.js
in a components
folder, and that folder is going to live in the same directory where index.html
lives so that our *.js
files are a level down from index.html.
+root dir
| index.html
|
\---components
MyComponent.js
MyComponent.js
(function () {
// We "export" the component by placing it in global scope.
// This could get messy, and you might want to "namespace" components, etc.
window.MyComponent = function (props) {
return html`<div>
<h3>#${props.displayText}#</h3>
<p>This is some normally-sized text.</p>
</div>`;
};
})();
And let's add that component to our index page.
index.html
<html>
<head>
<title>Hello, Preact World!</title>
<script src="https://unpkg.com/preact@10.11.3/dist/preact.umd.js"></script>
<script src="https://unpkg.com/htm@3.1.1/preact/standalone.umd.js"></script>
<script>
window.render = window.preact.render;
window.html = window.htmPreact.html;
document.addEventListener('DOMContentLoaded', function () {
render(
html`<${MyComponent} displayText="write this out on the screen, kk?" />`,
document.getElementById('root-element')
);
});
</script>
<script src="./components/MyComponent.js"></script>
</head>
<body>
<p>This is some pre-Preact text</p>
<div id="root-element"></div>
</body>
</html>
(Note the render
call in the script block that includes MyComponent
in some weird markup.)
And that's it! We're Preacting.
How did that work?
Okay, sure, that works. But... why?
There are a few things from that code that we should review to really know what we're doing.
IIFEs
First, note the Immediately Invoked Function Expression, or IIFE. An IIFE creates a new scope for your JavaScript code so that none of its local variables spill into the global scope.
But we do want our component type -- MyComponent
-- to be available everywhere, so we have to "export" it to the global scope (again, that's the browser's window
) with window.MyComponent =
.
So sorta along the lines of "Leave no trace" when you're camping, that means the only interaction our component has with the rest of our code is making MyComponent
available. We're leaving as little "trace" as possible, one export per component.
Hooks
Luckily, hooks are the new P/React coolness, as they utilize a more functional approach to creating components than React and classes initially did. Before, you'd have a class per component type.
Using classes isn't a huge deal, as most modern browsers have supported class
for a while. Here are compatibility charts for class
from MDN and caniuse.
But using object oriented classes can be a major head rethread for many older-school JavaScript programmers. I'd also argue using object/class-oriented paradigms in a client codebase is broken window that leads to developing overly complex client-side codebases. It's too powerful a paradigm for your client.
Hooks, on the other hand, are very simple. With a hooked component, I...
- push in
props
as the sole param to a function that's a constructor,
- sprinkle those properties' values into an html template,
- return the template rendered into html,
... and *poof*, we've made dynamic client-side code.
The functional programming side of hooks is that simple functions encourage a situation where putting the same props into the machine gives you get the same results out every time. There's no obvious room for side effects or complicated logic. Your component is literally a simple function -- the MyComponent
function -- which turns its props
into markup.
(That's the "props down" in the props down, events up paradigm. We'll talk about "events up" in the next lesson.)
Don't believe me that hooks are better than classes? Heck, even React itself is rewriting their docs to use hooks in every example!
We are rewriting the React documentation with a few differences:
- All explanations areย written using Hooksย rather than classes.
This really couldn't be much simpler:
function (props) {
return html`<div>
<h3>#${props.displayText}#</h3>
<p>This is some normally-sized text.</p>
</div>`;
};
Tagged template literals
Which brings us to the third thing to review: templates. Remember step 2, "sprinkle properties into an html template"? Doing so cleanly without transpilation isn't complicated, but takes some thought.
You'll note the return
value in the MyComponent
function uses template literals, a new es6 feature that allows special string values, even multiline strings, encased in backticks -- the `
character.
html`<div>
<h3>#${props.displayText}#</h3>
<p>This is some normally-sized text.</p>
</div>`
You'll also notice that the backticked string has html
before the first backtick, which is a strange prefix for a string. That makes it a tagged template, with html
as the "tag" function. That function processes the value of the string in the template literal before it's returned.
What happens, in a sense, is that the template is put through the same process JSX performs in conventional React. The difference is JSX doesn't need the backticks because it's going through a transpiler -- a build process -- that can preprocess our nonstandard JavaScript code (the JSX) however it likes.
For us, without a build process, we needed something that was JavaScript native. Tagged templates provide the same function JSX does elsewhere.
From mozilla.org:
Template literals are enclosed by backtick (`
) characters instead of double or single quotes.
Along with having normal strings, template literals can also contain other parts calledย placeholders, which are embedded expressions delimited by a dollar sign and curly braces:ย ${expression}
. The strings and placeholders get passed to a function โ either a default function, or a function you supply. [emphasis mine -mfn]
In our case, HTM's html
is that "function you supply".
But we do require a little more code -- the tag function, the backticks, AND the weird ${}
format around anything that should be processed as JavaScript.
As an example, if we were using JSX, MyComponent
's return would look like this:
return <div>
<h2>This is some text for a header</h2>
<p>This is some normally-sized text.</p>
<p>#{props.displayText}#</p>
</div>;
Not a huge difference, but certainly cleaner.
Hurry up & wait
The last thing to cover is this line:
document.addEventListener('DOMContentLoaded', // ...
That listener is just like $(document).ready( // ...
in jQuery and ensures the page is ready to be manipulated before we start fiddling with its content.
You can give the code a shot without that wrapper if you're curious.
Review
And that's a good stopping point for now.
We've gone through...
- How to import Preact and HTM libraries.
- How to push
render
and html
functions to the global scope.
- How to create a Preact component in an IIFE to keep scoping tighter and cleaner.
- How to render a Preact component to an html page.
- A little bit about what a p/React hook is.
- How HTM uses tagged template to create templates for Preact to render.
- How to use
document.addEventListener('DOMContentLoaded', // ...
to ensure we wait until the page is ready to render our content.
Though admittedly all we've created is a really boring, static page.
Next we'll discuss some best practices and how to introduce state to our system.
Labels: howto, javascript, preact, tutorial