-
Notifications
You must be signed in to change notification settings - Fork 469
Explore how to implement the ergonomic dynamic import #5698
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
Comments
Is this the (or a) idiomatic way to dynamically load a component? |
Looking randomly at TS resources, here's an example: async function renderWidget() {
const container = document.getElementById("widget");
if (container !== null) {
const widget = await import("./widget");
widget.render(container);
}
}
renderWidget(); Looks like pretty much the same example should be expressible. |
I guess |
@mattdamon108 just a high level brain dump here.
Making up random syntax etc, but this is how I would imagine the mechanism should instead behave at a high level: let foo = async (x) => {
switch x {
| A =>
module Lib = await LibA // dynamically load module LibA
Lib.someFunction("aaa")
| B =>
module Lib = await LibB // dynamically load module LibB
Lib.someFunction("bbb")
}
} |
If we're going to implement
Why should |
Is it valid? let foo = () => {
let m = LibA
m
} |
How about this without the constraint of being inside async context? // in async context
let foo = async (x) => {
switch x {
| A =>
module Lib = await @import LibA // dynamically load module LibA
Lib.someFunction("aaa")
| B =>
module Lib = await @import LibB // dynamically load module LibB
Lib.someFunction("bbb")
}
} // not in async
let foo = (x) => {
switch x {
| A =>
let ma = @import LibA // dynamically load module LibA
ma
| B =>
let mb = @import LibB // dynamically load module LibB
mb
}
} |
First try to fix your initial example. The code does not extract make. |
Yes, it doesn't extract make, because the module doesn't have an export default. |
Do we have a feature to make an export default BTW? @default let m = comp // something like? |
That's a separate question. It's "let default = ..." |
Let's consider the case where |
The second example tries to assign a module to a value variable |
Sure, I'll. |
The point I'm trying to make is that if the generated code needs to extract |
The fact that you need to extract a value from a promise and package it back into a promise: that's what I mean when I say that conceptually dynamic loading belongs to an async context. As an async context guides you do do exactly that without you even having to think about it. |
It makes sense. |
Let me check quickly to see if it works. var make = dynamic((function async (param) {
let m = await import("./LazyComp.bs.js");
return m.make
}), {
ssr: false
}); |
That seems correct except, I don't know what the |
Side comment: this React.lazy const OtherComponent = React.lazy(() => import('./OtherComponent')); Next.js const DynamicHeader = dynamic(() => import('../components/header'), {
suspense: true,
}) |
|
Therefore I had a bit frustration to make import only in the async context, but async/await is just a syntax sugar of Promise. So, that would be no problem, but just make sure if it works fine in runtime. |
What's the actual type of that? const DynamicHeader = dynamic(() => import('../components/header'), {
suspense: true,
}) it seems to me it takes a function that returns a promise. Correct? |
The examples I see in this answer do a lot of promise chaining: So that seems to confirm that it's conceptually in an async context. When I say conceptually I don't mean necessarily that it will be used via async-await, just that async-await seems a natural way to express it. The fact that even when you hide the promise inside Basically I would like to explore the basic building blocks needed to support dynamic loading in the language, in general. Whether or not you're using some lazy UI framework. |
Separately, there's the question of how to use Next and React with the least friction possible. |
Essentially, the difficulty has to do with the language and its treatment of modules as separate entities from values. The first question is: what is Without using async/await one needs to be a bit creative. So I guess these are essentially the 2 kinds of ways of doing it, each with their own trade-offs. |
@mattdamon108 thoughts about ^ ? |
Sorry, I'm back now. (serial meetings..) I think we are very closely standing on the same page now. I think we can start with the small step, that is
This is a very good question. In terms of language, how about this? let p0 = import(C.make) // C.make is a value. The semantic seems too different from the js?
let p1 = await import(C.foo) // C.bar is a value too transformed to var p0 = import("path of C").then(m => m.make)
var c = await import("path of C")
var p1 = c.bar |
I believe |
The reason I don't suggest |
I really like that. I think that the JS way of importing an entire module is just a consequence of dynamic import working only on files, not because it's the best possible API. So if we can support both importing the module itself, and importing a single value, I think we'll have great ergonomics. I imagine it's probably at least as common to reach for a single value rather than the full module when importing. Having to refer to the full module in JS, just to then remap it to what you're really after, seems mostly like (unnecessary) manual labor. |
Additionally, this syntax needs to work only in "package-specs": "module": "es6" |
If that's the assumption, it surely simplifies the back-end. |
The |
@mattdamon108 here's a skeleton to begin playing with, if you're interested in exploring further: |
I've investigated how to implement this before by myself, I'll follow your work and learn from it. Thanks for letting me know! |
Great, take what you need from it, and leave what you don't need. |
Another thought: it's pretty easy to load something dynamically, and also load it statically by mistake, by just writing other code that references the same module. |
Do you mean this? // in the same file
let lazyFoo = import(C.foo)
let lazyBar = import(C.bar) If so, why would the warning need to be emitted? |
Probably more this: // in the same file
let lazyFoo = import(C.foo)
let f = C.bar() |
@zth what do you think? Do we need to emit an warning? |
Probably not for an initial release, but it highlights an interesting issue that's also magnified by the fact that using things in ReScript is easier thanks to no explicit imports. Also probably worth checking if such warnings exists for eslint/tslint etc. |
Hi, I'm new here and don't know much about programming language design, I'm just expressing some of my thoughts here. Dynamic import is quite natural in js because it is a nice addition to the static import without semantic changes. import {foo} from "./foo"
const {foo} = await import("./foo") So I'm wondering if can we introduce something like dynamically opening modules into the language? I think it's more natural than dynamic import. open Foo
foo()
dopen Foo // compile to the dynamic import statement
foo() |
@ah-yu Isn't it more natural to js expression you show? let {foo} = import(module(C)) Can you elaborate the js expression corresponding to |
I think we typically try to stay quite close to JS, so that the concepts map as well as possible to each other across the languages. |
Forgot about Does close mean we should keep the same API with javascript? The module system of rescript is quite different from Js, so I thought there might have a more natural way to do a dynamic import. another thought(just something came into mind, not thinking about it deeply): let a = () => {
module Foo = dynamic Foo
Foo.foo()
} |
Actually, there is no api let a = async () => {
let {foo} = await import(module(Foo))
foo()
} Does it make sense to you? |
Thank you for your prompt reply. @mattdamon108 But the semantics are different. There is no import behavior in rescript. So the |
There wasn't |
Filling in missing details: first-class modules are more painful to use than that. This is how it looks like: module type FooT = module type of Foo
let a = async () => {
let d = await import(module(Foo: FooT))
module M = unpack(d)
M.foo()
} |
Might be better to have 2 different mechanisms after all, where the one for modules is this: let a = async () => {
module M = await import(Foo)
M.foo()
} Just to avoid pages of doc explaining what first-class modules are, and apologise about them. |
First class module means that it can be taken as argument to function, right? |
Unlike javascript, rescript can access modules without importing them. Why for dynamic modules we should import them first and then use them? It really doesn't make sense to me. Why not just mark them as dynamic |
import * as foo from "foo.js" // 1
let m = import("./foo.js").then(m => m.default) // 2 @ah-yu IMHO, 1 and 2 are different in js. ReScript can access other modules without 1. The dynamic loading modules means 2. |
Because you want to have control on when they are loaded. Which is not necessarily: the first time they are used. |
I see. Let me think about it. Anyway, thanks for your time and patience! @mattdamon108 |
I see. Thanks for the explanation. |
It means modules packaged as values. In order to package a module as a value you need to provide the signature (in the example, the full module type of the module). Then they can be used just like other values, in particular sent as arguments to functions. But in order to do anything with them, they need to be unpacked back into modules. |
Related #5593
Thanks to JSX v4, now we can write the dynamic import for react component:
This example passes the type checker as we intended, but the import statement in js output should be emitted differently.
As-is:
To-be:
The text was updated successfully, but these errors were encountered: