Fable: Can't introduce an interface for React props

Created on 12 Jun 2016  路  8Comments  路  Source: fable-compiler/Fable

I'm working on a demo using Redux with Fable (see here: https://github.com/psfblair/fable_redux_skeleton) but a type-related problem has me stymied.
I'm opening an issue because this is too long for Gitter, and I don't know of a mailing list or Google group for Fable; I hope that's not inappropriate.

Running the current project gives an error: "Required prop 'store' was not specified in 'Provider'." This is because creating a React-Redux Provider component requires React props to implement the ReactRedux.Property<'P> interface with two members: store and children.

My F# code implements the interface with a CounterProps type (a class or a record type, I've tried both); however, when I create the Provider the CounterProps instance doesn't have the required field. (See details below.)

The store field gets removed by the Fable helpers for React: R.com calls Fable.Helpers.React.toPlainJsObj on the props. This function calls getOwnPropertyNames on the props, which results in both store and children being removed (though internal fields such as store@17-1 and children@18-1 are not).

Even if I change R.com so that it doesn't call toPlainJsObj on the props, the problem is built-in to the way React creates elements. The function ReactElement.createElement takes a config (i.e., props) but then uses hasOwnProperty to create the props for the element from the config. Fields that are owned by the config's prototype are skipped.

Is there some way I can get the relevant fields in my data structure to be owned by the object itself rather than by the prototype, and still have the F# data structure implement an interface with that same signature?

Details

F# requires me to provide an implementation of an interface, whereas JS wants direct access to the fields with the same names. I've tried lots of different things - classes or records with an interface implementation either using properties or methods, mutable let bindings, explicitly declared getters and setters, etc.

The following works as well as any of the other things I tried:

type CounterProps(store : Redux.Store option, children : React.ReactElement<CounterProps> option) = 
    member val store = store
    member val children = children
    interface ReactRedux.Property<CounterProps> with 
        member val store = store
        member val children = children

This results in the following JS at runtime. As you'll see it generates some redundant functions (having the redundant non-interface members keeps me from having to do a lot of type casting in my code), but removing the redundant members makes no difference to the issue:

    var CounterProps = exports.CounterProps = class CounterProps {
        constructor(store, children) {
            var store_ = store;
            var children_ = children;
            this["store@17-1"] = store;
            this["children@18-1"] = children;
        }

        get store() {
            return this["store@17-1"];
        }

        get children() {
            return this["children@18-1"];
        }

        get store() {
            return this["store@17-1"];
        }

        get children() {
            return this["children@18-1"];
        }
    };

The error I get is:

warning.js:49Warning: Failed propType: Required prop `store` was not specified in `Provider`.

When the Provider component is created, the ReactElementValidator is called--it's a type checker that makes sure the object has the same fields as a given reference object. In this case it throws the error because store isn't there. The Provider's props look like this in the debugger (note the absence of a store field):

element.props: Object
   children:Object
   children@18-1:undefined
   store@17-1:Object
   __proto__:Object

Why is the store field removed? This is because of Fable.Helpers.React.toPlainJsObj which is called by R.com on the props. This function calls getOwnPropertyNames on the props, which results in ["store@17-1", "children@18-1"] As a result, both store and children are removed when the object is converted to plain JS. Later, when the Provider's child components are created the children field gets re-added to the object, but store is still absent.

bug

Most helpful comment

@psfblair the early bits of what I have is here: https://github.com/mastoj/Fable/tree/stuff/samples/browser/virtualdom

The sample is working, but I have a sample page to write :)

All 8 comments

Don't worry, this is the perfect place to report issues that are too long for Gitter :+1:

Unfortunately, these are the cases where frictions between different type models appear. In JS/TypeScript an object fulfils an interface if it has a field with the same name. In .NET/F# interface properties are always implemented as getters and Fable install these getters on the prototype (except for object expressions).

The method toPlainJsObj was included to prevent warnings from React (see #157). In principle, it was thought only for records. By design Fable installs F# record fields directly on the object to make them look more like POJOS (though they still have a specific prototype and can be type tested). But I acknowledge this is too restrictive, so I have modified the toPlainJsObj to transfer also getters from the prototype. Can you please give [email protected] a try and close the issue if it works for you now?

About the redundant properties, this is because Fable doesn't implement interfaces explicitly as stated in the docs. I was thinking to do it, but this would make interaction with JS more difficult in other places. In your case, it doesn't matter that the properties are overwritten because they actually have the same value, and I know that F# forcing casting to access interface members can be annoying. But if possible I would choose a different strategy to prevent conflicts in the future.

Working, thanks! Let's see if I can complete this without opening any more issues!

Great, thanks for the confirmation! Don't worry if you've to report more issues, they're really helpful to improve Fable :+1: Actually, in the new version of the React helper I'm not transferring functions but I just noticed sometimes it's necessary to do that, so I'll upload another version to remove the restriction.

At this point I've come to the point where I'm not able to get React to re-render components when a dispatch causes a change to the Redux state store, and I'm not sure why. Unfortunately, I've come to the end of the time I have budgeted to do this, so I won't be able to complete the demo. It's been really challenging trying to figure all this out, but it's involved much more time in the debugger scratching my head than I had anticipated.

I'm sorry to hear that but I really appreciate the time you've working on this and your contributions. It's true that it's difficult to make Fable interact with some JS libraries, particularly when these libraries are so-called frameworks. Sometimes is due to Fable's bug but other times these libraries rely on JS nuances that don't completely correspond to the generated code. Even if Fable tries to stay as close as possible to standard JS there'll always be some frictions 馃槙

I'll try to have a look at your demo to see if I can make it work. However, although I think it's valuable to be compatible with a rendering tool like React, your experience indicates that supporting architectures like Redux may not have so much benefits and it's better to design an architecture for Fable that takes advantage of F# features (immutability, asynchronous workflows) like the ClojureScript community has done with Om, for example.

In any case, thanks a lot for your hard work. I hope you can soon find some time to play again with Fable and keep contributing 馃憤

Yes, this experience has made me realize even more how much interop with Javascript libraries and frameworks is a Really Hard Problem, particularly for a statically and strongly typed language. You're to be commended for the work you're doing and I appreciate your responsiveness very much.

I really love Elm, which is the inspiration for Redux and Om, but unfortunately Elm's type system is a bit too restrictive for what I needed to do (it doesn't support full structural typing or interfaces). WebSharper has UI Next, but I'm really partial to the unidirectional dataflow model with all state in one place. If I had the time, I might dedicate myself to writing something similar for F#, but unfortunately I don't see that happening any time soon.

@mastoj is working in an Elm-like architecture using virtual-dom (the library behind Elm) to take advantage of F# characteristics and with isomorphic potential (using the same DSL to render views on client and server side). Hopefully when you have time to have a look again, this effort will have progressed and you can contribute to it :)

@psfblair the early bits of what I have is here: https://github.com/mastoj/Fable/tree/stuff/samples/browser/virtualdom

The sample is working, but I have a sample page to write :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

alfonsogarciacaro picture alfonsogarciacaro  路  37Comments

alfonsogarciacaro picture alfonsogarciacaro  路  57Comments

et1975 picture et1975  路  25Comments

grishace picture grishace  路  28Comments

chrisvanderpennen picture chrisvanderpennen  路  31Comments