developit / htm

Hyperscript Tagged Markup: JSX alternative using standard tagged templates, with compiler support.
Apache License 2.0
8.64k stars 169 forks source link

Hooks not working correctly using off-the-shelf htm@^3.1.0 and preact@^10.5.14 #209

Open integryiosama opened 3 years ago

integryiosama commented 3 years ago

I am using htm and Preact as follows inside a simple functional component. This component is imported by a class component where it is swapped out according to a switching logic with another component. Here is how I am importing the libraries:

import { html } from 'htm/preact';
import { useEffect, useRef, useState } from 'preact/hooks';

I have the latest installed versions of each package. Hooks appear to work fine on first mount, but when the component unmounts, any version of Preact past 10.2.0 is failing with the message TypeError: Cannot read property '__hooks' of undefined. If I install 10.2.0 explicitly then this error goes away. I assume it is because htm has this version in the devDependencies:

  "devDependencies": {
    "@babel/core": "^7.2.2",
    "@babel/preset-env": "^7.1.6",
    "@types/jest": "^26.0.24",
    "babel-jest": "^24.1.0",
    "babel-preset-env": "^1.7.0",
    "eslint": "^5.2.0",
    "eslint-config-developit": "^1.1.1",
    "jest": "^24.1.0",
    "microbundle": "^0.10.1",
    "preact": "^10.2.0",
    "react": "^16.8.3"
  }

I think there is a version mismatch happening but I cannot figure out how to fix it. I tried module resolution but that didn't help. My package.json has these dependencies:

  "dependencies": {
    "htm": "^3.1.0",
    "preact": "^10.5.14",
    "unfetch": "^4.2.0",
    "unistore": "^3.5.2"
  }

The combination which works is:

  "dependencies": {
    "htm": "^3.1.0",
    "preact": "10.2.0",
  }

Can someone please help me figure this out? Version 10.2.0 came more than a year ago and I would like to use the latest version.

developit commented 3 years ago

@integryiosama can you show how you're bundling this code? The error you provided happens when there is both a CommonJS and ES Modules version of Preact being used at the same time.

integryiosama commented 3 years ago

@developit thanks for the quick response!

I am using rollup. Here is a simplified version of my config. In dev mode, I use rollup-plugin-livereload and rollup watch mode to rebuild files. In dev mode only the UMD bundle is built, so I can directly use it in an html file to quickly iterate.

import ts from '@wessberg/rollup-plugin-ts';
import { terser } from 'rollup-plugin-terser';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import postcss from 'rollup-plugin-postcss';
import clear from 'rollup-plugin-clear';
import json from '@rollup/plugin-json';

import pkg from './package.json';

const outputDir = './dist';

const getPluginsConfig = (config) => {
  const { prodMode, minify } = config;

  const pluginArr = [
    postcss(),
    commonjs(),
    resolve({
      browser: true,
    }),
    json(),
    ts({
      transpiler: 'babel',
    }),
  ];
  if (minify) {
    pluginArr.push(
      terser({
        output: {
          comments: !prodMode,
        },
      }),
    );
  }
  return pluginArr;
};

export default () => {
  const enableProdMode = !!process.env.PROD_MODE_ENABLED;
  const shoudMinify = !!process.env.MINIFY_ENABLED;
  const shouldEnableHMR = !!process.env.HOT_RELOAD_ENABLED;

  const esmBundle = {
    input: 'src/index.ts',
    output: [
      {
        file: pkg.module,
        format: 'esm',
        sourcemap: true,
      },
    ],
  };

  const umdBundle = {
    input: 'src/index.umd.ts',
    output: [
      {
        file: pkg.main,
        format: 'umd',
        sourcemap: true,
        name: 'SomeName',
      },
    ],
  };

  esmBundle.plugins = [
    clear({
      targets: [`${outputDir}/esm`],
      watch: true,
    }),
    ...getPluginsConfig({
      prodMode: enableProdMode,
      minify: shoudMinify,
      hmr: shouldEnableHMR,
    }),
  ];

  umdBundle.plugins = [
    clear({
      targets: [`${outputDir}/umd`],
      watch: true,
    }),
    ...getPluginsConfig({
      prodMode: enableProdMode,
      minify: shoudMinify,
      hmr: shouldEnableHMR,
    }),
  ];

  const bundlesToBuild = [umdBundle];
  if (!shouldEnableHMR) bundlesToBuild.push(esmBundle);

  return bundlesToBuild;
};

I built a prod UMD bundle and replaced that in my html file, and the error changed to this:

index.js:706 Uncaught (in promise) Error: Hook can only be invoked from render methods.
    at Object.U.__h (index.js:706)
    at we (slicedToArray.js:6)
    at slicedToArray.js:6
    at xe (slicedToArray.js:6)
    at y.Kt [as constructor] (index.ts:38)
    at y.U [as render] (index.ts:30)
    at P (index.ts:30)
    at k (index.ts:30)
    at P (index.ts:30)
    at k (index.ts:30)
integryiosama commented 3 years ago

@integryiosama can you show how you're bundling this code? The error you provided happens when there is both a CommonJS and ES Modules version of Preact being used at the same time.

@developit here's a gist with the generated UMD build if that helps. Really appreciate you helping me out with this!

marvinhagemeister commented 2 years ago

@integryiosama just had a quick look and the bundle contains two copies of Preact. The concept of hooks in general requires a singleton underneath it, but the different copies of Preact don't know about each other. That's why the hooks don't work.