Failure when module has other variables exported
oddlyfunctional opened this issue · 1 comments
Whenever the module being loaded contains any other bindings at the top level besides make
and default
, the following runtime error occurs:
react-dom.development.js:11017 Uncaught TypeError: Cannot read property 'someVariable' of undefined
at Object../src/LazyHelloWorld.bs.js (LazyHelloWorld.bs.js:12)
at __webpack_require__ (bootstrap:63)
at Object../src/App.bs.js (App.bs.js:12)
at __webpack_require__ (bootstrap:63)
at __webpack_require__.t (bootstrap:161)
Cause
BuckleScript compiles top level declarations as exports from that module, but since the lazy module declares the component as undefined
and runs the include (val component);
statement in order to make the compiler accept that it has the same signature as the actual component, it attempts to copy those exports from the undefined
value:
// Generated by BUCKLESCRIPT, PLEASE EDIT WITH CARE
'use strict';
var React = require("react");
var include = undefined;
var make = React.lazy((function (param) {
return import("./HelloWorld.bs.js");
}));
var Lazy_someVariable = include.someVariable; // <== This is the culprit
var Lazy = {
someVariable: Lazy_someVariable,
make: make,
$$default: make
};
exports.Lazy = Lazy;
/* include Not a pure module */
Steps to reproduce
I'm taking the same example from the docs, making only the necessary change to reproduce the error:
/* HelloWorld.re */
let someVariable = "Hello world ";
[@react.component]
let make = (~name) => <h1> (ReasonReact.string(someVariable ++ name)) </h1>;
let default = make;
Notice I'm using a variable at the top level of the module in this demonstration, but module declarations also cause the same error.
The rest of the example remains exactly the same:
/* LazyHelloWorld.re */
module type T = (module type of HelloWorld);
[@bs.val] external component: (module T) = "undefined";
module Lazy: T = {
include (val component);
let make = ReLoadable.lazy_(() => DynamicImport.import("./HelloWorld.bs.js"));
let default = make;
};
/* App.re */
[@react.component]
let make = () => {
<React.Suspense fallback={<div> (ReasonReact.string("Loading ...")) </div>}>
<LazyHelloWorld.Lazy name="Zeus" />
</React.Suspense>;
};
Workarounds
For anyone struggling with this issue, I came up with two workarounds.
- Extract all top level declarations to a different module and
open
it at the top:
/* HelloWorld_Deps.re */
let someVariable = "Hello world ";
/* HelloWorld.re */
open HelloWorld_Deps;
[@react.component]
let make = (~name) => <h1> (ReasonReact.string(someVariable ++ name)) </h1>;
let default = make;
- Declare
component
as a global object (it doesn't matter which one since the values won't actually be used):
/* LazyHelloWorld.re */
module type T = (module type of HelloWorld);
[@bs.val] external component: (module T) = "window";
module Lazy: T = {
include (val component);
let make = ReLoadable.lazy_(() => DynamicImport.import("./HelloWorld.bs.js"));
let default = make;
};
Fixed in reason-loadable@1.0.0 : https://github.com/kMeillet/reason-loadable/releases/tag/1.0.0
Be aware that you can only use default export with React.lazy. You can still use your top level binding in your component like your example :
/* HelloWorld.re */
let someVariable = "Hello world ";
[@react.component]
let make = (~name) => <h1> (ReasonReact.string(someVariable ++ name)) </h1>;
let default = make;
Quick reminder : Reason-loadable is made to lazy load React component, not JSON or JavaScript code that isn't a React component.