Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[🐞] When try to load component dinamically via import I get error #2643

Closed
oceangravity opened this issue Jan 14, 2023 · 24 comments
Closed
Labels
STATUS-1: needs triage New issue which needs to be triaged TYPE: bug Something isn't working

Comments

@oceangravity
Copy link

oceangravity commented Jan 14, 2023

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 oceangravity added TYPE: bug Something isn't working STATUS-1: needs triage New issue which needs to be triaged labels Jan 14, 2023
@stackblitz
Copy link

stackblitz bot commented Jan 14, 2023

Fix this issue in StackBlitz Codeflow Start a new pull request in StackBlitz Codeflow.

@manucorporat manucorporat added this to the BACKLOG: Post v1.0 milestone Mar 21, 2023
@mrclay
Copy link

mrclay commented Apr 22, 2023

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.

@EggDice
Copy link

EggDice commented Apr 26, 2023

@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

@appinteractive
Copy link

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?

@mrclay
Copy link

mrclay commented Jul 14, 2023

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

@GrandSchtroumpf
Copy link
Contributor

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

@mhevery
Copy link
Contributor

mhevery commented Oct 23, 2023

  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 mhevery closed this as completed Oct 23, 2023
@appinteractive
Copy link

appinteractive commented Oct 23, 2023

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

@mhevery
Copy link
Contributor

mhevery commented Oct 24, 2023

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

@appinteractive
Copy link

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?

@wmertens
Copy link
Member

@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

@mhevery
Copy link
Contributor

mhevery commented Oct 25, 2023

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.

@appinteractive
Copy link

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 🙏

@victorlmneves
Copy link

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

@wmertens
Copy link
Member

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

@maiieul
Copy link
Contributor

maiieul commented Oct 31, 2023

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

@victorlmneves
Copy link

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

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

@maiieul
Copy link
Contributor

maiieul commented Oct 31, 2023

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

@dhnm
Copy link

dhnm commented Jul 7, 2024

Let's say I am using Qwik's Responsive Images1. 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.

Footnotes

  1. https://qwik.dev/docs/integrations/image-optimization/#responsive-images

@tgskiv
Copy link

tgskiv commented Aug 30, 2024

Hi!
The last example suggested by @maiieul in this issue seems not to be working anymore, just as the example in the documentation https://qwik.dev/docs/cookbook/glob-import/ does not work as well.

As @dhnm mentioned, we need the tool to dynamically load the images. I have data structures containing image file names and would really like to use the Qwik way to import those images, instead of manually specifying each.

I've made it work, but I still have a bug.

Please advise how to handle the error:

(index):366 QWIK ERROR Code(30): QRLs can not be resolved because it does not have
an attached container. This means that the QRL does not know where it belongs inside the DOM,
so it cant dynamically import() from a relative path.
/src/components/slides/tools/getlazyimagecomponent_server_metacomponent_yoh0mayxono.js?_qrl_parent=tools-icon.tsx GetLazyImageComponent_server_MetaComponent_Yoh0mAyxono Error: 
Code(30): QRLs can not be resolved because it does not have an attached container. This means that the QRL does not know where it belongs inside the DOM, so it cant dynamically import() from a relative path.
    at createAndLogError (http://localhost:5174/node_modules/@builder.io/qwik/dist/core.mjs?v=faddbb96:159:54)
    at logErrorAndStop (http://localhost:5174/node_modules/@builder.io/qwik/dist/core.mjs?v=faddbb96:108:17)
    at qError (http://localhost:5174/node_modules/@builder.io/qwik/dist/core.mjs?v=faddbb96:315:12)
    at Object.importSymbol (http://localhost:5174/node_modules/@builder.io/qwik/dist/core.mjs?v=faddbb96:333:23)
    at resolve (http://localhost:5174/node_modules/@builder.io/qwik/dist/core.mjs?v=faddbb96:8667:44)
    at resolveLazy (http://localhost:5174/node_modules/@builder.io/qwik/dist/core.mjs?v=faddbb96:8674:49)
    at http://localhost:5174/node_modules/@builder.io/qwik/dist/core.mjs?v=faddbb96:8678:39
    at qrl (http://localhost:5174/node_modules/@builder.io/qwik/dist/core.mjs?v=faddbb96:8606:30)
    at invokeApply (http://localhost:5174/node_modules/@builder.io/qwik/dist/core.mjs?v=faddbb96:4536:26)
    at invoke (http://localhost:5174/node_modules/@builder.io/qwik/dist/core.mjs?v=faddbb96:4528:24)

Steps to reproduce (maybe there are other ways if you know the nature of the problem):

  • Start with any other page
  • Navigate through the website
  • Click on the to the link to the page with the images
  • The next page loads for a few seconds and stops loading and reacting to user interaction, leaving the user on the previous page.

Images are loaded fine if you start with the page with images.

Here is my code.
The error appears somewhere outside the component.

import {
  $,
  type Component,
  component$,
  useSignal,
  useTask$,
} from '@builder.io/qwik';
import { server$ } from '@builder.io/qwik-city';
 
/*
This based on 
https://qwik.dev/docs/cookbook/glob-import/
https://github.com/QwikDev/qwik/issues/2643
*/

const metaGlobComponents: Record<string, any> = import.meta.glob(
  '/src/media/**/**.*',
  {
    import: 'default',
    query: 'jsx',
    eager: true,
  }
);
 
export const GetLazyImageComponent = server$((path: string) => {
  // possible paths:
  // 'file.png'
  // /images/technologies/svg/react_clean.svg
  const brokenDownPath = path.split('/').filter(pathPart => !!pathPart);
  let fullPath = `/src/media/images/technologies/active/${path}`;

  if (brokenDownPath[0]==='images') {
    fullPath = `/src/media/${brokenDownPath.join('/')}`
  }

  if (!metaGlobComponents[fullPath]) {
    return;
  }
  const val = metaGlobComponents[fullPath]();
  const MetaComponent = $(()=>val);
  return MetaComponent;
});

 
export default component$((props: any) => {
  const MetaComponent = useSignal<Component<any>>();

  useTask$(async () => {
    const qrlAsyncFunction = await GetLazyImageComponent(props.path)
    if (!qrlAsyncFunction) {
      return;
    }

    // eslint-disable-next-line qwik/valid-lexical-scope
    MetaComponent.value = (await qrlAsyncFunction) as unknown as Component<any>;
  });

  return <>{MetaComponent.value && <MetaComponent.value />}</>;
});

Thank you!

@maiieul
Copy link
Contributor

maiieul commented Aug 30, 2024

Hi @tgskiv 👋 I assume it stops working for you in 1.8.0?

@tgskiv
Copy link

tgskiv commented Sep 1, 2024

Hi @maiieul thanks for the response and sorry for not specifying it in the comment.
It was 1.7.2, now 1.8.0 but it still can be reproduced.

@wmertens
Copy link
Member

wmertens commented Sep 1, 2024

@tgskiv can you try with eager false?

@lexasq
Copy link

lexasq commented Oct 3, 2024

@vmertens I'm currently proceeding the same task. With eager false it just doesn't load images, with eager true everything seems ok. BUT I have a usecase where I show the same images on 2 pages, 1 carousel, other is tiles.

Carousel is static and images load normally, but tiles are dynamically paginated and if I switch to this kind of import and try to navigate to the second page I receive "[vite] Internal server error: Dynamic import() inside Qrl($) scope is not a string, relative paths might break"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
STATUS-1: needs triage New issue which needs to be triaged TYPE: bug Something isn't working
Projects
None yet
Development

No branches or pull requests