fable-compiler / fable-react

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

How to make a custom HOC? #121

Closed Voronar closed 5 years ago

Voronar commented 5 years ago

I am trying to do the custom HOC. There is the code:

type FunctionComponent<'a> = 'a -> Fable.Import.React.ReactElement
type EnhanceFunction<'a> = FunctionComponent<'a> -> 'a -> seq<Fable.Import.React.ReactElement> -> Fable.Import.React.ReactElement

type DivComponentProps = { value: string; ClassName: string;}
let enhanceDiv: EnhanceFunction<DivComponentProps> = fun comp -> fun props ->
        React.ofFunction comp { props with ClassName = props.ClassName + " red" }

type SpanComponentProps = { text: string; ClassName: string; } 
let enhanceSpan: EnhanceFunction<SpanComponentProps> = fun comp -> fun props ->
    React.ofFunction comp { props with ClassName = props.ClassName + " red" }

let SpanComponent (props: SpanComponentProps) =
    React.span [
        ClassName props.ClassName
    ] [ React.str props.text ]

let DivComponent (props: DivComponentProps) =
    React.div [
        ClassName props.ClassName
    ] [ React.str props.value ]

let EnhancedDiv = enhanceDiv DivComponent
let EnhancedSpan = enhanceSpan SpanComponent

How to make the general enhance function instead of enhanceDiv and enhanceSpan ones?

P.S Playground for testing current code.

alfonsogarciacaro commented 5 years ago

Hmm, the problem here is that F# doesn't have ad-hoc polymorphism. Sometimes it's possible to emulate it using inline functions and SRTP, but here you need access both to the member and the constructor so the syntax can become very cumbersome (not sure if it's even possible).

If you don't mind using interfaces, you can achieve a similar effect with flexible types:

type IAddClass<'T> =
    abstract AddClass: string -> 'T

type DivComponentProps =
    { value: string; ClassName: string }
    interface IAddClass<DivComponentProps> with
        member this.AddClass c = { this with ClassName = this.ClassName + " " + c }

type SpanComponentProps =
    { text: string; ClassName: string }
    interface IAddClass<SpanComponentProps> with
        member this.AddClass c = { this with ClassName = this.ClassName + " " + c }

let enhance comp (props: #IAddClass<'props>) =
    React.ofFunction comp (props.AddClass "red")

let SpanComponent (props: SpanComponentProps) =
    React.span [
        ClassName props.ClassName
    ] [ React.str props.text ]

let DivComponent (props: DivComponentProps) =
    React.div [
        ClassName props.ClassName
    ] [ React.str props.value ]

let EnhancedDiv = enhance DivComponent
let EnhancedSpan = enhance SpanComponent
Voronar commented 5 years ago

That is to say what I can't make generic implementation of this code piece:

member this.AddClass c = { this with ClassName = this.ClassName + " " + c }

and I must duplicate the implementation for all types, that implements interface IAddClass?

alfonsogarciacaro commented 5 years ago

Hmm, not that I can think of. Maybe there is a way using something like FSharpPlus but I'm not sure. AFAIK there's no way to make record constructors generic. If you prefer not to use interfaces, I think the common way is to pass the transform in a parameter (as with a functor):

type DivComponentProps =
    { value: string; ClassName: string }

type SpanComponentProps =
    { text: string; ClassName: string }

let enhance comp (addClass: 'props->string->'props) (props: 'props) =
    React.ofFunction comp (addClass props "red")

let SpanComponent (props: SpanComponentProps) =
    React.span [
        ClassName props.ClassName
    ] [ React.str props.text ]

let DivComponent (props: DivComponentProps) =
    React.div [
        ClassName props.ClassName
    ] [ React.str props.value ]

let EnhancedDiv = enhance DivComponent (fun p c ->
    { p with ClassName = p.ClassName + " " + c })

let EnhancedSpan = enhance SpanComponent (fun p c ->
    { p with ClassName = p.ClassName + " " + c })
Voronar commented 5 years ago

According to this declined (by @dsyme) suggestion and my speculations about the subject I think a truly React HOC via record props is impossible even in a future.