Search code examples
javascriptreactjsnpmmoduleheadless-ui

Components importing HeadlessUI throwing errors when built and then used in another project


I am currently developing a UI Library using React, Tailwind, HeadlessUI and Storybook. Once I finished the components, and made sure that they worked in Storybook and on a react dev server, I built them and packaged them into a private npm package.

Most of them work fine when I import them in a test project using the created package, but some don't. All of the not-working components share one thing in common: Importing something from @headlessui/react.
(The working components don't import anything from headlessui)

When I try using these components in my test project, they all throw the same error:
(Here I chose the component 'Select' as an example)

Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports. Check the render method of 'Select'.

The error doesn't really make sense, because those components are exported from my library and imported into my test project exactly the same as the components that work. I even checked that by using a simple console.log on all components. It came out the same.
Here is how they are imported and exported:

// imported into test project:
import { Select, Form, Dropdown, /* ... */ } from "@my-package";
//exported from the library in index.js:
// ...
import Select from "./components/Select";
export { Select, /* ... */ };

Because I am sure that I imported them correctly into my test project, i am certain that it must have something to do with the build or HeadlessUI.

For building I am using babel.
Although babel never threw errors while building, I figured that I'd still post the confs here.
My build script: "build": "cross-env BABEL_ENV=production babel src -d dist"
My .babelrc:

{
  "presets": [
    [
      "react-app",
      {
        "absoluteRuntime": false
      }
    ]
  ],
  "plugins": ["babel-plugin-react-css-modules"]
}

Here is some of the non-built and built code from the component 'Select':

// Select.jsx
import React, { Fragment, useState } from "react";
import { Listbox, Transition } from "@headlessui/react";
import { CheckIcon, SelectorIcon } from "@heroicons/react/solid";
import PropTypes from "prop-types";

function classNames(...classes) {
  return classes.filter(Boolean).join(" ");
}

export default function Select({
  //...
}) {
  const selects = options;
  let selectedOption;
  let i = 0;
  selects.map((select) => {
    if (select.default) {
      selectedOption = selects[i];
      return true;
    }
    i++;
    return false;
  });

  const [selected, setSelected] = useState(selectedOption);

  return (
    <div className={className}>
      <Listbox value={selected} onChange={setSelected}>
        {({ open }) => (
          <>
            <Listbox.Label className="block text-sm font-medium text-gray-700">
              {title}
            </Listbox.Label>
            <div className="mt-1 relative">
              <Listbox.Button
                className={` bg-white relative border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-primary/60 focus:border-primary sm:text-sm ${
                  !length || length === "md"
                    ? "w-72"
                    : length === "fit"
                    ? "w-full"
                    : length === "lg"
                    ? "w-96"
                    : length === "sm" && "w-44"
                }`}
              >
              // ...
// Select.js
"use strict";

var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = Select;
exports.getSelected = getSelected;

var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/esm/slicedToArray"));

var _react = _interopRequireWildcard(require("react"));

var _react2 = require("@headlessui/react");

var _solid = require("@heroicons/react/solid");

function Select(_ref) {
// ...
return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_react2.Listbox.Label, {
      className: "block text-sm font-medium text-gray-700"
    }, title), /*#__PURE__*/_react.default.createElement("div", {
      className: "mt-1 relative"
    }, /*#__PURE__*/_react.default.createElement(_react2.Listbox.Button, {
      className: " bg-white relative border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-primary/60 focus:border-primary sm:text-sm ".concat(!length || length === "md" ? "w-72" : length === "fit" ? "w-full" : length === "lg" ? "w-96" : length === "sm" && "w-44")
    }, /*#__PURE__*/_react.default.createElement("span", {
      className: "block truncate ".concat(placeholder && !selected ? "text-gray-400" : "text-gray-700")
    }, selected ? selected.text : placeholder), /*#__PURE__*/_react.default.createElement("span", {
      className: "absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"
    }, /*#__PURE__*/_react.default.createElement(_solid.SelectorIcon, {
      className: "h-5 w-5 text-gray-400",
      "aria-hidden": "true"
    }))), // ...

Quick Summary:

  • Components that import headlessui don't render/work like intended.
  • The fault is probably not the importing/exporting but rather the building or HeadlessUI.

I am not sure what to make of this.
Any help is appreciated, thanks in advance!


Solution

  • The problem is the way the headless-ui package.json is referencing the entry point. In the node_modules folder in your custom component after you've installed it you will see a reference to _react2 = require('@headlessui/react'). If you look at the value of that it is a string path to file that doesn't exist:

    '/static/media/index.bad31bab1c06d3ff486c.cjs'

    If you look at the package.json file for this library you will see it points to a cjs file when being required. Because most applications/components these days are built with webpack, babel, etc it will always use the require:

    enter image description here

    If you go into that built file in your component and force it to be a module by adding the line of code below so it picks up the .js file you will see it work. So this is not an answer to the question because this is something the library owners are going to have to fix, it is just the reason you are seeing the issue.

    enter image description here

    The issue is