Instruction file imported from daniel-le97/banned-cli (
.github/instructions/preact-no-build.instructions.md). Copyright stays with the author.
Preact No-Build Setup Guidelines
This project uses Preact with no build step for frontend development. Follow these patterns for consistency and best practices.
Basic Usage
MDN has a great deal of information on how to utilize import maps, but a basic example looks like the following:
<!DOCTYPE html>
<html>
<head>
<script type="importmap">
{
"imports": {
"preact": "https://esm.sh/preact@10.23.1",
"htm/preact": "https://esm.sh/htm@3.1.1/preact?external=preact"
}
}
</script>
</head>
<body>
<div id="app"></div>
<script type="module">
import { render } from "preact";
import { html } from "htm/preact";
export function App() {
return html` <h1>Hello, World!</h1> `;
}
render(html`<${App} />`, document.getElementById("app"));
</script>
</body>
</html>
We create a <script> tag with a type="importmap" attribute, and then define the modules we'd like to use inside of it as JSON. Later, in a <script type="module"> tag, we can import these modules using bare specifiers, similar to what you'd see in Node.
Important: We use ?external=preact in the example above as https://esm.sh will helpfully provide the module you're asking for as well as its dependencies -- for htm/preact, this means also providing a copy of preact. However, Preact must be used only as a singleton with only a single copy included in your app.
By using ?external=preact, we tell esm.sh that it shouldn't provide a copy of preact, we can handle that ourselves. Therefore, the browser will use our importmap to resolve preact, using the same Preact instance as the rest of our code.
Recipes and Common Patterns
While not an exhaustive list, here are some common patterns and recipes you may find useful when working with import maps.
For these examples we'll be using https://esm.sh as our CDN -- it's a brilliant, ESM-focused CDN that's a bit more flexible and powerful than some others, but by no means are you limited to it. However you choose to serve your modules, make sure you're familiar with the policy regarding dependencies: duplication of preact and some other libraries will cause (often subtle and unexpected) issues. For esm.sh, we address this with the ?external query parameter, but other CDNs may work differently.
Preact with Hooks, Signals, and HTM
<script type="importmap">
{
"imports": {
"preact": "https://esm.sh/preact@10.23.1",
"preact/": "https://esm.sh/preact@10.23.1/",
"@preact/signals": "https://esm.sh/@preact/signals@1.3.0?external=preact",
"htm/preact": "https://esm.sh/htm@3.1.1/preact?external=preact"
}
}
</script>
Aliasing React to Preact
<script type="importmap">
{
"imports": {
"preact": "https://esm.sh/preact@10.23.1",
"preact/": "https://esm.sh/preact@10.23.1/",
"react": "https://esm.sh/preact@10.23.1/compat",
"react/": "https://esm.sh/preact@10.23.1/compat/",
"react-dom": "https://esm.sh/preact@10.23.1/compat",
"@mui/material": "https://esm.sh/@mui/material@5.16.7?external=react,react-dom"
}
}
</script>
HTM (Hyperscript Tagged Markup)
Whilst JSX is generally the most popular way to write Preact applications, it requires a build step to convert the non-standard syntax into something browsers and other runtimes can understand natively. Writing h/createElement calls by hand can be a bit tedious though with less than ideal ergonomics, so we instead recommend a JSX-like alternative called HTM.
Instead of requiring a build step (though it can use one, see babel-plugin-htm), HTM uses Tagged Templates syntax, a feature of JavaScript that's been around since 2015 and is supported in all modern browsers. This is an increasingly popular way to write Preact apps and is likely the most popular for those choosing to forgo a build step.
HTM supports all standard Preact features, including Components, Hooks, Signals, etc., the only difference being the syntax used to write the "JSX" return value.
import { useState } from "preact/hooks";
import { html } from "htm/preact";
function Button({ action, children }) {
return html` <button onClick=${action}>${children}</button> `;
}
function Counter() {
const [count, setCount] = useState(0);
return html`
<div class="counter-container">
<${Button} action=${() => setCount(count + 1)}>Increment<//>
<input readonly value=${count} />
<${Button} action=${() => setCount(count - 1)}>Decrement<//>
</div>
`;
}
render(html`<${Counter} />`, document.getElementById("app"));
Project-Specific Guidelines
- Use import maps for module resolution instead of full URLs in JavaScript files
- Always use
?external=preactwhen importing Preact-dependent modules from esm.sh - Prefer HTM syntax over JSX for template literals (no build step required)
- Keep components in separate files with
.jsextensions for better organization - Use semantic HTML and accessible patterns in component templates
- Follow modern ES modules patterns with explicit imports/exports