'How to use useState hook with Preact and no build tools?

Currently I need to get a Preact application working without any build tools, just with an index.html with the import statements to get preact from the CDN. I'm able to import the useState hook from the CDN no problem, and even able to console.log() the value of the function useState, but whenever I try to use it, I get an error saying:

'Uncaught TypeError: u is undefined'

Do ya'll happen to know why this is the case? I have tried to use the useState function inside of the functional component, and outside and it doesn't work either way. Am I missing something here? Can anyone help point me in the right direction?

<!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>Document</title>
    <script type="module">
        import { h, Component, render } from 'https://unpkg.com/preact?module';
        import { useState } from 'https://unpkg.com/preact@latest/hooks/dist/hooks.module.js?module'
        import htm from 'https://unpkg.com/htm?module';

        // Initialize htm with Preact
        const html = htm.bind(h);
      
        const App = (props) => {

            const [testVar, setTestVar] = useState(0);

            var countVariable = 0;

            const incrementButtonHandler = () => {
                countVariable = countVariable + 1;
            }

            const logMethod = () => {
                console.log(countVariable);
                countVariable = countVariable + 1;
            }

          return html`<div>
            <h1>Test ${props.name}!: ${countVariable}</h1>
            <button onClick=${logMethod}>Increment</button>
          </div>`;
        }
      
        render(html`<${App} name="World" />`, document.body);
      </script>
</head>
<body>
    
</body>
</html>


Solution 1:[1]

This is a known bug and limitation in what unpkg can do, see: https://github.com/preactjs/preact/issues/2571

There's a couple easy fixes though:

  1. Use already resolved URLs for each module (notice the @latest in the preact import)
import { h, render } from 'https://unpkg.com/preact@latest?module'
import { useState } from 'https://unpkg.com/preact@latest/hooks/dist/hooks.module.js?module'
import { html } from 'https://unpkg.com/htm/preact/index.module.js?module'
  1. Use a CDN without this issue, like skypack, instead:
import { h, render } from 'https://cdn.skypack.dev/preact';
import { useState } from 'https://cdn.skypack.dev/preact/hooks';
import { html } from 'https://cdn.skypack.dev/htm/preact';

Either should work.

Solution 2:[2]

rchristian's answer is useful, but the module-imported hooks appear to fail when using a router. Here's a minimal repro*.

(If you have trouble navigating to the /about route in the snippets, you may need to run them locally since the iframe doesn't play well with routing on all browsers, notably Chrome at the time of writing.)

<script type="module">
import {h, render, Component} from "https://cdn.skypack.dev/preact";
import {useState} from "https://cdn.skypack.dev/preact/hooks";
import {Router} from "https://cdn.skypack.dev/preact-router";
import htm from "https://cdn.skypack.dev/htm";
const html = htm.bind(h);

const Counter = () => {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(prevState => prevState + 1);
  };

  return html`
    <button onClick=${increment}>
      Count: ${count}
    </button>
  `;
};

const Nav = () => html`
  <nav>
    <a href="/">home</a> | <a href="/about">about</a>
  </nav>
`;
const Home = () => html`
  <p>Home</p>
  <${Counter} />
`;
const About = () => html`
  <p>About</p>
`;
const App = () => html`
  <header>
    <${Nav} />
  </header>
  <main>
    <${Router}>
      <${Home} path="/" />
      <${About} path="/about" />
      <${Home} default />
    <//>
  </main>
`;

render(html`<${App} />`, document.querySelector("#app"));
</script>
<div id="app"></div>

Starting at the home route, you'll see the counter component works fine. Navigate to the about route, then back to home and you'll see Uncaught (in promise) TypeError: u is null from the hooks script.

I'd much prefer the module imports so I'd be curious to hear how to fix the above code, but I did manage to work around this with the following UMD approach:

<script type="module">
const {h, render, Component} = preact;
const {useState, useEffect} = preactHooks;
const {Router} = preactRouter;
import htm from "https://cdnjs.cloudflare.com/ajax/libs/htm/3.1.1/htm.module.js";
const html = htm.bind(h);

const Counter = () => {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(prevState => prevState + 1);
  };

  return html`
    <button onClick=${increment}>
      Count: ${count}
    </button>
  `;
};

const Nav = () => html`
  <nav>
    <a href="/">home</a> | <a href="/about">about</a>
  </nav>
`;
const Home = () => html`
  <p>Home</p>
  <${Counter} />
`;
const About = () => html`
  <p>About</p>
`;
const App = () => html`
  <header>
    <${Nav} />
  </header>
  <main>
    <${Router}>
      <${Home} path="/" />
      <${About} path="/about" />
      <${Home} default />
    <//>
  </main>
`;

render(html`<${App} />`, document.querySelector("#app"));
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/preact/10.7.2/preact.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/preact/10.7.2/hooks.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/preact-router/4.0.1/preact-router.umd.min.js"></script>
<div id="app"></div>

* This is a known issue as developit has a couple of code sandbox repros:

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2