QwikDev / qwik

Instant-loading web apps, without effort
https://qwik.dev
MIT License
20.84k stars 1.31k forks source link

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

Closed oceangravity closed 1 year ago

oceangravity commented 1 year ago

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

stackblitz[bot] commented 1 year ago

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

mrclay commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago
  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.

appinteractive commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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

maiieul commented 1 year ago

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

Drawbacks:

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 commented 1 year ago

@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 commented 1 year ago

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

dhnm commented 4 months ago

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.

tgskiv commented 2 months ago

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):

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 commented 2 months ago

Hi @tgskiv ๐Ÿ‘‹ I assume it stops working for you in 1.8.0?

tgskiv commented 2 months ago

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 commented 2 months ago

@tgskiv can you try with eager false?

lexasq commented 1 month ago

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