fable-compiler / fable-react

Fable bindings and helpers for React and React Native
MIT License
275 stars 67 forks source link

How to use render props with prop getters (e.g. Downshift) #132

Closed cmeeren closed 5 years ago

cmeeren commented 5 years ago

I'm stuck trying to create bindings for Downshift. Have a look at the first example in the "Usage" section of the readme.

In short: The way Downshift works, is that the child of <Downshift> isn't a react component, but a function that takes an object – let's call it StateAndHelpers – with various state and prop getters and returns a react element. That way, you can customize exactly how things are rendered, but let Downshift control the state and supply necessary props to the building blocks you want.

Now, consider for example the following simplified code from the usage example:

<li {...getItemProps({key: item.value})}>{item.value}</li>

getItemProps is a property on the StateAndHelpers object. It is a function which returns an object that contains necessary props for the list item component, and you pass it other props (such as key above) that are merged into the final props for the list item component.

What I am struggling with, is that getItemProps returns an obj, not seq<IHTMLProp>. The Fable react helpers accept seq<IHTMLProp>, not obj. So it seems I can't use the li [] [] syntax, because I have no way (that I know of) of going from obj to seq<IHTMLProp>.

Are there good solutions to using a React component like Downshift in Fable, or is this bound to be a painful process?

cmeeren commented 5 years ago

I found a way to do it, based on this function to get the properties of an object as a (string * obj) list:

[<Emit("
var keys = [];
for(var key in $0){ keys.push([key, $0[key]]); }
return keys;
")>]
let toKeyValueList (o: obj) : (string * obj) list = jsNative

With this, I am able to expose e.g. getInputProps with the signature seq<IHTMLProp> -> seq<IHTMLProp> (instead of obj -> obj) like this (taken out of context and somewhat modified for clarity):

let fableGetInputProps (props: seq<IHTMLProp>) =
  props
  |> keyValueList CaseRules.LowerFirst  // convert to obj
  |> downshiftGetInputProps  // downshiftGetInputProps has signature obj -> obj
  |> toKeyValueList  // function defined above, we now have (string * obj) list
  |> Seq.map (fun kv -> kv |> HTMLAttr.Custom :> IHTMLProp)  // Convert to seq<IHTMLProp>

There's some more boilerplate in the bindings regarding converting necessary objects/functions between the public and downshift versions, but this is hidden from users.

Closing this, but do let me know if this is a bad way to solve the problem.

cmeeren commented 5 years ago

For the record, the bindings are now published as Fable.Import.Downshift.

alfonsogarciacaro commented 5 years ago

OMG, why every React component library needs to invent their own way to pass props and children? 😬As commented here the Fable.React helpers use F# lists to be more idiomatic, but if you need to operate with raw prop objects, you can define your own domEl that doesn't apply the keyValueList transformation. For example:

let inline rawDomEl (tag: string) (props: obj) (children: ReactElement seq): ReactElement =
    createElement(tag, props, children)

// Usage
let render rawProps =
    rawDomEl "div" rawProps [p [] [str "A text"]
cmeeren commented 5 years ago

Yes, that could work, too. But I'd like the public API when used from Fable to be consistent with the F# list style. I'd say I managed to do this in a very nice way, which you can see from the Fable.Import.Downshift documentation and the comment above. :)

What will happen with render props (such as used by Downshift) now that hooks are a thing is another matter, of course.