I’m trying to get code splitting to work but don’t see a React.lazy
function. When I searched for “lazy” I also couldn’t find anything relevant in this forum or on the RescriptReact github issues about it. Is this something I need to roll-my-own for, maybe with external
?
While working on the bindings for solidJs
, I also had to find a way to bind the lazy
function provided there.
Afterwards I made a short test and the bindings also work for react
. It turned out the function signatures are basically identical. These are the bindings I came up with (pretty sure there is some improvement potential there, but so far they work well):
module Suspense = {
@module("react") @react.component
external make: (~fallback: React.element, ~children: React.element, unit) => React.element =
"Suspense"
}
module Lazy = {
module type T = {
let make: React.component<unit>
let makeProps: unit => unit
}
type dynamicImport = Js.Promise.t<{"make": React.component<unit>}>
@module("react")
external lazy_: (unit => Js.Promise.t<{"default": React.component<unit>}>) => React.component<unit> =
"lazy"
let make: (unit => dynamicImport) => module(T) = func => {
let lazyPromise = lazy_(() => func()->Js.Promise.then_(comp => {
Js.Promise.resolve({"default": comp["make"]})
}, _))
module Return = {
let make = lazyPromise
let makeProps = () => ()
}
module(Return)
}
}
@val
external import_: string => Lazy.dynamicImport = "import"
In general, handling dynamic imports with ReScript is problematic, since you cannot reference the .res
files directly. You have to load the generated .bs.js
files instead. But this works okay as long as you use the in-source: true
option in bsconfig.json
. (At least for me using vite
)
This is how you could use it in a component:
@react.component
let make = () => {
let module(Comp) = Bindings.Lazy.make(() => Bindings.import_("./Component.bs"))
<Comp />
}
I also created a tiny example repo to try it out: rescript-react-lazy-example
The bindings only support components without props
but in general it would be possible to change the Lazy module into a Functor that would allow the passing of props
to components.
Building the bindings for components with props is a little more complicated than the simple example above. The main challenge is to make the component have the right type without having to define all types by hand (even when using a Functor).
Luckily ReScript supports the type of
operator for modules which makes it a lot more usable. But still there is a lot of “magic” necessary to make dynamic imports work with components.
I created a new repository that contains minimal bindings for React.lazy. It also supports component with props. You can check it out here:
I didn’t have much time to test it in depth. If you find any problems with it, just let me know.
Thank you for this @Fattafatta. I’ve been playing around with your code for a week and this is what I came up with that would both compile and work in the browser, so I thought I would share with everyone:
To surmount the type issue we can add component-specific bindings to import
and React.lazy
, along with a type for the shape of the props, or unit
if there are no props. As an example I’ll use a Loading
component that shows a spinner and takes optional props for a color and a label:
//child to lazy load
type propShape = {"fillColor": option<string>, "label": option<string>}//or unit if no props
@val
external import_: string => Promise.t<{"make": React.component<propShape>}> = "import"
@module("react")
external lazy_: (unit => Promise.t<{"default": React.component<propShape>}>) => React.component<propShape> = "lazy"
@react.component
let make = (~fillColor="fill-stone-100", ~label="") => {
<>...</>
}
Then in a parent component where you want to use the lazy-loaded component:
//parent lazy-loading the child
@react.component
let make = (props) => {
let loading = React.createElement(
Loading.lazy_(() =>
Loading.import_("./Loading.bs")->Promise.then(comp => {
Promise.resolve({"default": comp["make"]})
})
),
Loading.makeProps(~label="lobby...", ()),//or just () if no props
)
...
<div>
<React.Suspense fallback=React.null> loading </React.Suspense>
</div>
}
Since I didn’t output a new module we can’t use JSX (at least, I couldn’t find a way), so we store a call to React.createElement
in a variable. The signature of createElement
is (component<'props>, 'props) => element
. We are lazy-loading the component and then to get the props we call the component’s makeProps
function, which is available to other components along with make
. The return value of makeProps
must match the propShape
defined in the component. Don’t forget to end the call to makeProps
with a unit ()
. If the child has no props, just use a unit ()
instead of calling its makeProps
function. Then, as required by React, we place the variable holding the component in a React.Suspense
(binding from rescript-react).
I tried to make a functor out of this but since it had to be called with the module you want to lazy-load, the component would be imported in the JS, negating the lazy-load and defeating the purpose. Note that the descendants of a lazy-loaded component do not have to be lazy-loaded themselves to avoid being requested on initial page load.
//compiled JS
The compiled lazy-loaded child component does not change since we just add a type and two externals
. If we try to parameterize the file name for the call to import
by exporting it from the child component, this works but also kills the lazy-load.
The compiled parent component does not change much. It has one fewer import for the lazy-loaded child and no additional currying. The child’s instantiation and use look like this:
var loading = React.createElement(React.lazy(function (param) {
return import("./Loading.bs").then(function (comp) {
return Promise.resolve({
default: comp.make
});
});
}), {
label: "games..."
});
...
React.createElement(
React.Suspense, {
children: loading,
fallback: null
}
)
The child component is not requested by the browser until it is needed by the parent and there are no console errors, so it seems to work! Lemme know what you think!
Dynamic import has now landed (in the latest ReScript 11 alpha version), and I just opened a PR for React.lazy
.