qwik: [๐Ÿž] When try to load component dinamically via import I get error

Which component is affected?

Qwik Rollup / Vite plugin

Describe the bug

Hi ๐Ÿ˜Š

Currently, I can import any component by this way, the normal way:

import { $, component$, useClientEffect$, useStore } from "@builder.io/qwik";
import ComponentA from "~/components/component-a";
import ComponentB from "~/components/component-b";
import ComponentC from "~/components/component-c";

export default component$(() => {
  const tree = useStore(
    [
      { tag: "ComponentA", type: 1 },
      { tag: "ComponentB", type: 1 },
      { tag: "div", type: 0, class: "bg-green-400", content: "Hello" },
    ],
    {
      recursive: true,
    }
  );

  const changeComponent = $(() => {
    tree[0].tag = "ComponentC";
  });

  useClientEffect$(() => {
    // @ts-ignore
    window.changeComponent = changeComponent;
  });

  const components: Record<string, any> = {
    ComponentA: ComponentA,
    ComponentB: ComponentB,
    ComponentC: ComponentC,
  };

  return (
    <>
      <div>
        <div>
          {tree.map((element) => {
            if (element.type === 0) {
              const Tag = element.tag as any;
              return <Tag class={element.class}>{element.content}</Tag>;
            }

            if (element.type === 1) {
              // Works fine
              // const Component = components[element.tag];
             
              // Works fine
              // const Component = await import(`~/components/component-a`)

              // Fail 
              const Component = await import(`~/components/${element.tag}`)

              return <Component key={element.tag} />;
            }
          })}
        </div>

        <button onMouseDown$={changeComponent}>Click me</button>
      </div>
    </>
  );
});

I tried it with success:

const Component  = await import(`~/components/component-a`)

But, if I wanna import some component dynamically (async) like:

const Component  = await import(`~/components/${element.tag}`)

It fails ๐Ÿ˜ช with error:

[plugin:vite-plugin-qwik] Dynamic import() inside Qrl($) scope is not a string, relative paths might break

In Vite, you can pass it with /* @vite-ignore */ comment, but I tried it too with no success.

Is there some way to achieve successfully this?

Reproduction

https://stackblitz.com/edit/qwik-starter-qnzpvu?file=src%2Fcomponents%2Fcomponent-b.tsx,src%2Fcomponents%2Fcomponent-c.tsx,src%2Froutes%2Flayout.tsx

Steps to reproduce

npm install && npm start

System Info

System:
    OS: Linux 5.0 undefined
    CPU: (8) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
    Memory: 0 Bytes / 0 Bytes
    Shell: 1.0 - /bin/jsh
  Binaries:
    Node: 16.14.2 - /usr/local/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 7.17.0 - /usr/local/bin/npm
  npmPackages:
    @builder.io/qwik: ^0.15.2 => 0.15.2 
    @builder.io/qwik-city: ^0.0.128 => 0.0.128 
    vite: 3.2.4 => 3.2.4

Additional Information

No response

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 1
  • Comments: 18 (6 by maintainers)

Most upvoted comments

@mrclay If the dynamicity is not about the on-demand loading but the programable loading by name as here, then I think it is still relevant

Yes, the above is possible with caveats.

  1. During build time, the qwik build system needs to have access to all components so that it can generate symbol IDs for it.
  2. There needs to be a hashmap (could be on the server) that maps the name to a component.

Here is one way to do in Qwik: https://stackblitz.com/edit/qwik-starter-j55lca (but depending on your requirements it may be done differently)

import { Component, component$, useSignal, useTask$ } from '@builder.io/qwik';
import { server$ } from '@builder.io/qwik-city';

export const getComponentFromDB = server$((name: string) => {
  return {
    cmpA: CompA,
    cmpB: CompB,
  }[name];
});

export const CompA = component$(() => <span>A</span>);
export const CompB = component$(() => <span>B</span>);

export default component$(() => {
  const Comp = useSignal<Component<any>>();
  const name = useSignal('cmpA');
  useTask$(async ({ track }) => {
    const compName = track(() => name.value);
    Comp.value = await getComponentFromDB(compName);
  });
  return (
    <div>
      <input type="text" bind:value={name} />
      <div>dynamic component: {Comp.value && <Comp.value />}</div>
    </div>
  );
});

@mhevery hey I mean the path or name of a component is read dynamically for the current user or page and I need to load it dynamically. So like mentioned by @oceangravity:

const Component  = await import(`~/components/${nameFromDatabase}`)

Is this possible with Qwik? I find that issue a lot, like if everyone is just building static sites but in big apps you need to load components dynamically based on information related to the user or a product.

@appinteractive

Sorry for the oversight, itโ€™s actually possible to import.meta.glob without eager:true

Rectified example:

type ComponentPreviewProps = QwikIntrinsicElements["div"] & {
  name: string;
  align?: "center" | "start" | "end";
  language?: "tsx" | "html" | "css";
};

export const ComponentPreview = component$<ComponentPreviewProps>(
  ({ name, align = "center", language = "tsx", ...props }) => {
    const config = useConfig();
    const highlighterSignal = useSignal<string>();

    const componentPath = `/src/registry/${config.value.style}/examples/${name}.tsx`;

    const Component = useSignal<Component<any>>();
    const ComponentRaw = useSignal<string>();

    useTask$(async () => {
      const highlighter = await setHighlighter();

      Component.value = (await components[componentPath]()) as Component<any>;
      ComponentRaw.value = (await componentsRaw[componentPath]()) as string;

      highlighterSignal.value = highlighter.codeToHtml(
        ComponentRaw.value || "",
        {
          lang: language,
        }
      );
    });

    return (
      <div>
          ...
          {Component.value && <Component.value />}
          <div dangerouslySetInnerHTML={highlighterSignal.value} />
      </div>
    );
  }
);

This doesnโ€™t seem to add much to dev server starting time and it does allow me to improve my mdx editing DX quite a lot.

So in my .mdx files,

instead of doing

import CardWithFormPreview from "~/registry/new-york/examples/card-with-form";
import CardWithFormCode from "~/registry/new-york/examples/card-with-form?raw";

<ComponentPreview code={CardWithFormCode}>
  <CardWithFormPreview q:slot="preview" />
</ComponentPreview>

where I have to add weird conditional logic with Slots if I want to pass different components (in my case I also have /registry/default, so I would have to find a way to display the right components based on user config).

I can simply do

<ComponentPreview name="card-with-form" />

And let my component handle everything for me ๐Ÿ‘Œ .


@mhevery what do you think of import.meta.glob as an alternative to dynamic import? If itโ€™s not an issue for the optimizer I think it should be presented in the docs as it can significantly improve the DX for some use cases (especially in .mdx files where thereโ€™s no typescript auto-complete).

This would require a bit more testing (especially in prod), but I can work on a docs PR if you like the idea.

@appinteractive it is possible to make a vite plugin that creates such a registry object from a directory of Qwik components. But manually maintaining the registry probably takes less time than writing and maintaining the plugin

This error happens even with no dynamic content. As long as youโ€™ve got a back quote in the import() function, vite throw the error:

[vite] Internal server error: Dynamic import() inside Qrl($) scope is not a string, relative paths might break
  Plugin: vite-plugin-qwik
  File: <...>/icon.tsx:24:21
  24 |    const res = await import(`./icons/material/zoom_in.txt?raw`);
     |                       ^
  25 |    return res.default;
  26 |  });

Itโ€™s my understanding that if you build Qwik components correctly, thereโ€™s no reason to use dynamic import(). The in-browser runtime loads everything on-demand already.