Search code examples
javascripttypescriptimportcomponentsexport

Component throwing error "type is invalid" when imported to another project


So I am making a Storybook 7 Typescript component library with React. And obviously I am also importing this library into another project using a private NPM package.

One of my components, namely the "Select" component, is throwing this error when I import it into said project:

Uncaught Error: Element type is invalid: expected a string (for built-in components) or a class/function ... Check the render method of Select.

Now the first thing I did was check my imports in the library, but nothing seems to be off, let alone different from all the other components, which seem to be loading just fine. Even more so strange, the component works perfectly when I load it up in Storybook,

I am currently building a website with this component library and this component is the only problem causer. My best guess is that it has something to do with the imports inside the Select component, but I checked those from the files they were exported from and they seemed to be just fine as well.

Here is the code of Select.tsx:


import React, { Fragment, useState } from 'react';
import { Listbox, Transition } from '@headlessui/react';
import { CheckIcon, SelectorIcon } from '@heroicons/react/solid';

function classNames(...classes: string[]) {
  return classes.filter(Boolean).join(' ');
}

type Option = {id?: string | any;
  text?: string | any;
  default?: boolean;}

// interface to declare all the prop types
export interface Props {
  options?: Option[];
  title?: string | any;
  length?: 'sm' | 'md' | 'lg' | 'fit';
  placeholder?: string;
  className?: string;
  onChange: (value: string) => void;
}

// Select component
export const Select: React.FC<Props> = ({
  options,
  title,
  className,
  length,
  placeholder,
  onChange
}) => {
  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<any>(selectedOption);

  return (
    <div className={className}>
      <Listbox value={selected} onChange={(value) => {
        setSelected(value);
        onChange(value);
      }}>
        {({ open }) => (
          <>
            <Listbox.Label className="block text-sm font-medium text-gray">
              {title}
            </Listbox.Label>
            <div className={`relative`}>
              <Listbox.Button
                className={`cursor-pointer bg-white relative border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left 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'
                }`}
              >
                <span
                  className={`block truncate ${
                    placeholder && !selected ? 'text-gray-400' : 'text-gray'
                  }`}
                >
                  {selected ? selected.text : placeholder}
                </span>
                <span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
                  <SelectorIcon
                    className="h-5 w-5 text-primary"
                    aria-hidden="true"
                  />
                </span>
              </Listbox.Button>

              <Transition
                show={open}
                as={Fragment}
                leave="transition ease-in duration-150"
                leaveFrom="opacity-100"
                leaveTo="opacity-0"
              >
                <Listbox.Options
                  className={`absolute z-10 mt-1 bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm ${
                    !length || length === 'md'
                      ? 'w-72'
                      : length === 'fit'
                      ? 'w-full'
                      : length === 'lg'
                      ? 'w-96'
                      : length === 'sm' && 'w-44'
                  }`}
                >
                  {selects?.map(select => (
                    <Listbox.Option
                      key={select.id}
                      id={select.id}
                      className={({ active }) =>
                        classNames(
                          active ? 'text-white bg-primary' : 'text-gray-900',
                          'cursor-pointer select-none relative py-2 pl-3 pr-9'
                        )
                      }
                      value={select}
                    >
                      {({ selected, active }) => (
                        <>
                          <span
                            className={classNames(
                              selected ? 'font-semibold' : 'font-normal',
                              'block truncate'
                            )}
                          >
                            {select.text}
                          </span>

                          {selected ? (
                            <span
                              className={classNames(
                                active ? 'text-white' : 'text-primary',
                                'absolute inset-y-0 right-0 flex items-center pr-4'
                              )}
                            >
                              <CheckIcon
                                className="h-5 w-5"
                                aria-hidden="true"
                              />
                            </span>
                          ) : null}
                        </>
                      )}
                    </Listbox.Option>
                  ))}
                </Listbox.Options>
              </Transition>
            </div>
          </>
        )}
      </Listbox>
    </div>
  );
};

Now again keep in mind, that it works perfectly in Storybook.

This is how I export the component from the index.ts:


import { Select } from './components/Select';
// ...
export { ..., Select };

And here's how I import the component into another component of the library. Keep in mind, that I actually only use the Select component through SearchResultHeader but i also tried importing the Select component on its own but the outcome was the same.


import { ..., SearchResultHeader, Select } from "@naos/ioe-ui-library";

Thanks in advance to everyone that answers!


Solution

  • The problem is with the SVG icons from heroicons:

    import { CheckIcon, SelectorIcon } from '@heroicons/react/solid';
    

    Remove these SVG icons and you will be able to load the component.

    You can add a fix for this by importing as follows:

    const normalizeIcon = (Icon: typeof SvgIcon) => {
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
        return ((Icon as any).default ? (Icon as any).default : Icon) as typeof SvgIcon
    }
    
    const CheckIconFix = normalizeIcon(CheckIcon)
    

    And then use the normalized component.

    <CheckIconFix...
    

    This works for @mui/icons-material/Check.js I think the issue relates to the way the icon is exported in Check.js. For example, in @mui/icons-material/Check.js

    "use strict";
    "use client";
    
    var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
    Object.defineProperty(exports, "__esModule", {
      value: true
    });
    exports.default = void 0;
    var _createSvgIcon = _interopRequireDefault(require("./utils/createSvgIcon"));
    var _jsxRuntime = require("react/jsx-runtime");
    var _default = (0, _createSvgIcon.default)( /*#__PURE__*/(0, _jsxRuntime.jsx)("path", {
      d: "M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"
    }), 'Check');
    exports.default = _default;
    

    The icon is being created using a function called createSvgIcon, which is called using _interopRequireDefault from @babel/runtime. I think this is confusing the import system.