Fable 2.0 + React, constructor "as this" notation producing errors in console

Created on 12 Oct 2018  路  6Comments  路  Source: fable-compiler/Fable

Description

When using Fable 2.0 + Fable.React, and using the as this notation in the constructor in order to call this.setInitState or similar code, the JavaScript generated is broken.
Seems to be related to: https://github.com/fable-compiler/Fable/issues/1506

Repro code

  1. Download and extract the sample: fable2_react.zip
  2. Run yarn install
  3. Run yarn start
  4. Go to http://localhost:8080/ and open up the console. There should be no errors present.
  5. Edit the file src/App.fs and change type TestComp(initialProps) = to type TestComp(initialProps) as this =
  6. Refresh the page
  7. There will be a couple of errors in the console as described below

Error detail

The relevant errors that show up are:

Warning: The <TestComp /> component appears to have a render method, but doesn't extend React.Component. This is likely to cause errors. Change TestComp to extend React.Component instead.

Uncaught TypeError: Cannot set property 'contents' of undefined
    at TestComp (main.js:32819)
    at mountIndeterminateComponent (react-dom.development.js:13745)
    at beginWork (react-dom.development.js:14069)
    at performUnitOfWork (react-dom.development.js:16416)
    at workLoop (react-dom.development.js:16454)
    at HTMLUnknownElement.callCallback (react-dom.development.js:145)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:195)
    at invokeGuardedCallback (react-dom.development.js:248)
    at replayUnitOfWork (react-dom.development.js:15745)
    at renderRoot (react-dom.development.js:16548)
Uncaught TypeError: Cannot set property 'contents' of undefined
    at TestComp (main.js:32819)
    at mountIndeterminateComponent (react-dom.development.js:13745)
    at beginWork (react-dom.development.js:14069)
    at performUnitOfWork (react-dom.development.js:16416)
    at workLoop (react-dom.development.js:16454)
    at renderRoot (react-dom.development.js:16533)
    at performWorkOnRoot (react-dom.development.js:17387)
    at performWork (react-dom.development.js:17295)
    at performSyncWork (react-dom.development.js:17267)
    at requestWork (react-dom.development.js:17155)



md5-7757d74db16a804cb6dc6987dcb8e344



import { FSharpRef, declare, Record } from "./fable-core.2.0.3/Types";
import { createElement, Component } from "react";
import { int32ToString } from "./fable-core.2.0.3/Util";
import { render as render$$1 } from "react-dom";
export const TestProps = declare(function TestProps(arg1) {
  this.x = arg1 | 0;
}, Record);
export const TestComp = declare(function TestComp(initialProps) {
  const $this$$1 = this;
  const this$ = new FSharpRef(null);
  new Component(initialProps);
  $this$$1.contents = $this$$1;
  $this$$1["init@12-5"] = 1;
});
export function TestComp$$$$002Ector$$Z59F5D2D(initialProps) {
  return this != null ? TestComp.call(this, initialProps) : new TestComp(initialProps);
}

TestComp.prototype.render = function () {
  const this$$$1 = this;
  return createElement("div", {}, ...["value: " + int32ToString(this$$$1.props.x)]);
};

export function Test() {
  return createElement(TestComp, new TestProps(123), ...[]);
}
render$$1(Test(), document.getElementById("app_container"));

Related information

  • Fable version (dotnet fable --version): 2.0.3
  • Operating system: Win10 x64

EDIT: Forgot to post my workaround for the moment which is to not bind via as this in the constructor, and instead simply use base.setInitState() instead which works fine and has no errors.

Most helpful comment

Hmm, this is giving me headaches since the first versions of Fable. When using as x in constructors the F# compiler generates code that doesn't make sense in JS but is very difficult to identify and remove (all my attempts to do it so far have been fruitless). Moreover, it's not a good practice to use it so I'm considering to just throw an error message and tell users to use base instead if they're trying to call a base method. What do you think?

All 6 comments

Hmm, this is giving me headaches since the first versions of Fable. When using as x in constructors the F# compiler generates code that doesn't make sense in JS but is very difficult to identify and remove (all my attempts to do it so far have been fruitless). Moreover, it's not a good practice to use it so I'm considering to just throw an error message and tell users to use base instead if they're trying to call a base method. What do you think?

I think an error message would make sense (it definitely would work fine for the React-component use case here), though I would worry that there may be some more advanced use case for it that isn't immediately obvious.

So I ran into a need to call a member method in the constructor today (mainly for some signaling via a callback in this.props), and it would definitely be nice to have this syntax, though it can be worked around by moving what is essentially just a utility method outside the class and having the caller pass this, so definitely not a show-stopper, but this is only possible because React.Component has all public members, so there definitely is a bit of a use case for the syntax.

I'm thinking of using this as an excuse to dive into the internals of Fable a bit though (most compilers seem a bit boring, but Fable seems like it would be some fun 馃榿), and see if I can come up with anything myself with regard to this particular issue. Are there any instructions for how to build Fable itself and where I should start reading the code?

It'd be great if you become acquainted with Fable internals! Only thing is I'm not sure if this is the best introduction because it involves dealing with oddities of the F# AST. Actually I just gave it another look and think I came up with something that may work #1610. Would you happen to have another test we could add to the suite before merging the PR?

About instructions, there are a few things here and here but unfortunately nothing very structured yet. The three main modules which account for the three main transformation steps (F#>Fable, Fable optimizations, Fable>Babel) are:

src/dotnet/Fable.Compiler/Transforms/FSharp2Fable.fs
src/dotnet/Fable.Compiler/Transforms/FableTransforms.fs
src/dotnet/Fable.Compiler/Transforms/Fable2Babel.fs

Besides that, Replacements is also an important module as it replaces calls to FSharp.Core or the BCL with inlined code or calls to fable-core JS files (whose source is in src/js/fable-core).

Hmmm I am having trouble removing React from the sample (the issue doesn't appear with just regular classes, it seems). I will do some more investigation with minimal samples and try to reproduce the issue without Fable.React. I'll post back here with my findings and hopefully a sample that doesn't depend on React.

Took me quite a while to get something that would work, but here is a non-React example:

open Fable.Core.JsInterop

type Base() = class end

type Test() as _this =
    inherit Base()

let test = (!!jsConstructor<Test> : unit -> Test)()

If you paste this into the Fable REPL2 (http://fable.io/repl2/) and run it, you will observe the error in the browser console (but not the docked REPL console):
Cannot set property 'contents' of undefined

So here's what happens with the React example I attached to the first post:

  1. as this causes Fable to fail to generate the second parameter to declare for TestComp, should be declare(function TestComp(...) { ... }, Component) instead of just declare(function TestComp(...) { ... })
  2. This causes React to incorrectly identify the passed component constructor as a function-style component instead of a class-style component.
  3. Because it thinks it's a simple function, it calls TestComp(props) instead of new TestComp(props) which causes this to be undefined, resulting in the other errors.

I have confirmed that if the output of Fable is modified to be declare(function TestComp(...) { ... }, Component), then the error goes away.

Here's how the example snippet above captures this without React:

  1. as _this (underscore is needed to avoid warning about unused) causes Fable to fail to generate the second parameter to declare for Test, should be declare(function Test(...) { ... }, Base) instead of just declare(function Test(...) { ... })
  2. We then call the constructor incorrectly like React would (as explained above) by using jsConstructor<TestComp> and calling it without new. This is exactly what React.ofType does when it calls React.createElement:
let inline ofType<'T,'P,'S when 'T :> Component<'P,'S>> (props: 'P) (children: ReactElement seq): ReactElement =
#if FABLE_COMPILER
    createElement(jsConstructor<'T>, props, children)
#else
    createServerElement(typeof<'T>, props, children, ServerElementType.Component)
#endif

This isn't exactly a comprehensive test though, since you probably need a test that actually checks the generated JavaScript instead of something that always fails in a weird way. I could probably write something that would do a check like that, but I'm not sure how you like to test these sort of things.

It seems that the easiest way would be to have a simple setup like above with Test inheriting from Base, use as this, and then have some JavaScript that double checks that Test.prototype instanceof Base is true in the generated code.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rommsen picture rommsen  路  3Comments

jwosty picture jwosty  路  3Comments

MangelMaxime picture MangelMaxime  路  3Comments

et1975 picture et1975  路  3Comments

tomcl picture tomcl  路  4Comments