arialpew/reason-loadable

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.

  1. 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;
  1. 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.