preactjs / preact

⚛️ Fast 3kB React alternative with the same modern API. Components & Virtual DOM.

Home Page:https://preactjs.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Using useState leads to error: Uncaught (in promise) TypeError: Cannot read properties of undefined (reading '__H')

afh opened this issue · comments

  • Check if updating to the latest Preact version resolves the issue
    Already using the latest released version of Preact, i.e. 10.20.1, as of filing this issue.

Describe the bug
Preact shows error Uncaught (in promise) TypeError: Cannot read properties of undefined (reading '__H') when useState is used.

To Reproduce

Steps to reproduce the behavior:

  1. With a file named test.tsx containing the following content:
/** @jsxImportSource https://esm.sh/preact@10.20.1 */
import { h } from "https://esm.sh/preact@10.20.1";
import { useState } from "https://esm.sh/preact@10.20.1/hooks";
import renderPreactToString from "https://esm.sh/preact-render-to-string@6.4.1";

const App = () => {
  const [count, setCount] = useState(23)
  setCount(42);
  return h('p', {}, count)
}

console.log(renderPreactToString(h(App, {})));
  1. Using Deno 1.41.3
deno --version
deno 1.41.3 (release, aarch64-apple-darwin)
v8 12.3.219.9
typescript 5.3.3
  1. Running deno run test.tsx results in:
error: Uncaught (in promise) TypeError: Cannot read properties of undefined (reading '__H')
    at l (https://esm.sh/stable/preact@10.20.1/denonext/hooks.js:2:205)
    at R (https://esm.sh/stable/preact@10.20.1/denonext/hooks.js:2:339)
    at I (https://esm.sh/stable/preact@10.20.1/denonext/hooks.js:2:308)
    at Object.App (file:///$PWD/test.tsx:7:29)
    at C (https://esm.sh/v135/preact-render-to-string@6.4.1/denonext/preact-render-to-string.mjs:2:5489)
    at q (https://esm.sh/v135/preact-render-to-string@6.4.1/denonext/preact-render-to-string.mjs:2:3764)
    at file:///$PWD/test.tsx:12:13

Expected behavior
I'd expect to see <p>42</p>.

What am I doing or is going wrong?

The way esm.sh works is that it creates a full bundle for every package that also includes all of its dependencies. This means that you're ending up with two different copies of Preact. In this case one is 10.20.1 and the other is 10.20.0. You can check this for yourself by adding "vendor": true to your deno.json and inspecting the contents of the generated vendor/ directory.

Normally, duplicate dependencies wouldn't be a problem, but because hooks rely by design on a singleton pattern to keep track of the currently rendering component (not a choice by us), this breaks when one component is rendered by one version of Preact and another by another version of Preact.

This is a bit of a limitation of URL imports and tools like esm.sh which are not aware of the other imports in your code. You'll pretty much always will end up with duplicate dependencies without knowing about it, without passing some additional options to tools like esm.sh. The solution to this is to ensure that that only ever one copy of Preact is present in your app. This can be done through an import map, either in the browser or inside deno.json:

// deno.json
{
  "imports": {
    "preact": "https://esm.sh/preact@10.20.1",
    "preact/": "https://esm.sh/preact@10.20.1",
    "preact-render-to-string": "https://esm.sh/preact-render-to-string@6.4.1?external=preact"
  }
}

Note the ?external=preact bit to the last URL as this tells esm.sh not to inline its own copy of Preact into that file.

Since you're already using Deno, you could set up JSX as well to get the full benefits of a typesafe templating language:

  {
    "imports": {
      "preact": "https://esm.sh/preact@10.20.1",
      "preact/": "https://esm.sh/preact@10.20.1/",
      "preact-render-to-string": "https://esm.sh/preact-render-to-string@6.4.1?external=preact"
    },
+  "compilerOptions": {
+    "jsx": "react-jsx",
+    "jsxImportSource": "preact"
+  }
  }

With that setup, you can simplify your snippet a bit:

- /** @jsxImportSource https://esm.sh/preact@10.20.1 */
- import { h } from "https://esm.sh/preact@10.20.1";
  import { useState } from "preact/hooks";
- import renderPreactToString from "preact-render-to-string";
+ import { renderToString } from "preact-render-to-string";
  
  const App = () => {
    const [count, setCount] = useState(23)
    setCount(42);
-   return h('p', {}, count)
+   return <p>{count}</p>
  }

- console.log(renderPreactToString(h(App, {})));
+ console.log(renderToString(<App />));

Thank you for the quick and elaborate response, @marvinhagemeister, very helpful and very much appreciated! 🙏

To make things work I had to import { useState } from "preact/hooks";

Happy to help. Whoops, yeah that import should've been preact/hooks. Thanks for noticing, I've updated the snippet.