qwik icon indicating copy to clipboard operation
qwik copied to clipboard

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

Open oceangravity opened this issue 2 years ago โ€ข 3 comments

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

oceangravity avatar Jan 14 '23 18:01 oceangravity

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.

mrclay avatar Apr 22 '23 18:04 mrclay

@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

EggDice avatar Apr 26 '23 23:04 EggDice

Same issue here. I want to load components (images as with .jsx ) dynamically as the data for loading is represented as a string in the database.

How am I able to load those based on this key dynamically? This is a fundamental requirement for larger projects.

@EggDice @oceangravity did one of you guys had any success with this?

appinteractive avatar Jul 14 '23 09:07 appinteractive

I wonder if you could use a dynamic import inside https://qwik.builder.io/api/qwik/#useresource or https://qwik.builder.io/docs/components/tasks/#usetask

mrclay avatar Jul 14 '23 16:07 mrclay

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 |  });

GrandSchtroumpf avatar Oct 22 '23 08:10 GrandSchtroumpf

  1. All components are already loaded dynamically. So there is nothing to do here and no reason to lazy load them.
  2. Any imports that are NOT relative should already work. (Import starting with ./ or ../ will not work (and can't work))
  3. Imports such as icons/material/zoom_in.txt?raw can not work because they are vite tricks and only exist during build/dev time, not once the application is in production.

Same issue here. I want to load components (images as with .jsx ) dynamically, as the data for loading is represented as a string in the database.

I don't understand. If it is in the database, then import() will not help you, and you need to use some other RPC mechanism.

I am going to close this issue because this is either not needed or is working as intended. If you want to lazy load and you don't fall into categories 1, 2, or 3 above, please create a new issue.

mhevery avatar Oct 23 '23 15:10 mhevery

@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 avatar Oct 23 '23 15:10 appinteractive

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 avatar Oct 24 '23 14:10 mhevery

Ahh, that's interesting, thank you Miลกko.

But when you have a lot of components, would there be a possibility to get all components under a specific directory? That's how Vite solves it as far as I remember, so when Vite (or was it webpack?) detects a variable in an import statement, it does that hash map for you at build/dev time. Would be the most elegant solution I think, or is there something that would speak against such an approach?

appinteractive avatar Oct 24 '23 15:10 appinteractive

@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

wmertens avatar Oct 24 '23 16:10 wmertens

In order to support resumability, the code has to go through an optimizer and each function needs to get a hash both on server and client. So there is more to it than just "loading"

So in general all lazy loaded code needs to be available to the optimizer at the time of compilation.

mhevery avatar Oct 25 '23 14:10 mhevery

In order to support resumability, the code has to go through an optimizer and each function needs to get a hash both on server and client. So there is more to it than just "loading"

So in general all lazy loaded code needs to be available to the optimizer at the time of compilation.

That's out of question, just wondered if the imports could be generated from items like SVGs, images or components inside a specific directory by pointing to a path + var without the need of creating an index file containing all assets by hand.

But that is possible is already perfectly fine, just a bit cumbersome maybe to work with in case of updates, aka "Developer Experience" or "DRY" but it's more flexible I guess.

Thanks for clarifying though ๐Ÿ™

appinteractive avatar Oct 26 '23 14:10 appinteractive

Hi all I'm trying to implement let's say something similar. The project where I'm currently working uses a server-driven UI architecture but for a "regular website" and not for a mobile app using Vue 3. This means that we don't build pages, only components, and then the page is built based on JSON returned from the server.

E.g:

{
  "name": "New Dashboard",
  "appearance": "Scene",
  "data": {
    "id": "unique-page-id-123",
    "layout": "MasterLayout",
    "title": "Dasboard",
    "description": "",
    "keywords": "",
    "components": [
      {
        "name": "counter",
        "appearance": "counter",
        "data": {
          "id": "unique-stage-1"
        }
      },
      {
        "name": "work",
        "appearance": "work",
        "data": {
          "id": "1",
          "type": null,
          "client": "Ficaat",
          "project": "Layout and Responsive HTML catalogue made to run on tablets (2013)",
          "description": "Layout and Responsive HTML catalogue made to run on tablets (2013)",
          "slug": "ficaat-tablet",
          "image": "ficaat_app.jpg"
        }
      }
    ]
  }
}

I already have it working when running dev mode but the problem comes when running the preview as it tries to load the TSX files and not a built js module.

Is there any way to make it work, or what I'm trying to do is not possible with Qwik?

https://stackblitz.com/edit/github-zlgpzh-2safe5?file=src%2Froutes%2Fdynamic%2F[slug]%2Findex.tsx,src%2Futils%2Fload-component.ts To see it working in dev mode, just click on the hamburger menu and then click on "Dynamic Scene"

Thanks

victorlmneves avatar Oct 29 '23 12:10 victorlmneves

@victorlmneves Make your dynamic component into a switch that imports each component separately

wmertens avatar Oct 30 '23 13:10 wmertens

@appinteractive

But when you have a lot of components, would there be a possibility to get all components under a specific directory?

Something like import.meta.glob might be what you're looking for. It works both for importing components and their ?raw value.

Example :

const components = import.meta.glob("/src/registry/new-york/examples/*", {
  import: "default",
  eager: true,
});
const componentsCodes = import.meta.glob("/src/registry/new-york/examples/*", {
  as: "raw",
  eager: true,
});

type ComponentPreviewProps = QwikIntrinsicElements["div"] & {
  name: string;
  align?: "center" | "start" | "end";
  code?: string;
  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 = components[componentPath] as Component<any>;

    useTask$(async () => {
      const highlighter = await setHighlighter();
      const code = componentsCodes[componentPath];

      highlighterSignal.value = highlighter.codeToHtml(code, {
        lang: language,
      });
    });
    
    return (
          <div>
          ...
          <Component />
          <div dangerouslySetInnerHTML={highlighterSignal.value} />
      </div>
    )
  }
)

Benefits:

  • I don't have to import those components files by hand -> +1 for the DX since I have a lot of components (~80) and I expect to have more in the future.

Drawbacks:

  • It seems to take quite a toll on the dev server. ~40 components roughly add 7 seconds for the components and 7 seconds for their ?raw value for the dev server to show the page. I expect this to increase as the number of components increases. -> -1 for the DX.

This doesn't seem to affect performance once the dev server is up and running. I haven't had the ability/time to test this in production yet (because of a qwik-ui bug).

For my use case the drawbacks outweigh the benefits. +15 seconds or more every time I run pnpm dev is not worth it for me. I think I'll be better off by importing the components where they're needed and passing them through with a Slot. I think you should be able to do the same even with your user config coming from the database.

maiieul avatar Oct 31 '23 11:10 maiieul

@victorlmneves Make your dynamic component into a switch that imports each component separately

@wmertens not sure if I got it. Can you detail? Thanks

victorlmneves avatar Oct 31 '23 12:10 victorlmneves

@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 :ok_hand: .


@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.

maiieul avatar Oct 31 '23 21:10 maiieul

Let's say I am using Qwik's Responsive Images^1. I have a number of images I want to optimize, and instead of manually importing each of them:

import Image1 from "./image1.jpg?jsx"
import Image2 from "./image2.jpg?jsx"
import Image3 from "./image3.jpg?jsx"
// etc.

I would like to do it in a bit more concise way with import(), eg.:

const images = imagePaths.map(path => import(path).then(module => module.default))

I can't think of any alternative ways of doing this currently.

dhnm avatar Jul 07 '24 10:07 dhnm