fable-compiler / fable-react

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

FunctionComponent.Of with generics doesn't call memoizeWith #162

Open leolorenzoluis opened 5 years ago

leolorenzoluis commented 5 years ago

Given I have the following code

let test<'T> =
  FunctionComponent.Of((fun (props: {|Dispatch: Msg -> unit; test: 'T|}) ->
        log props.test
        span [] []
          ]), memoizeWith= fun o n -> log "haha"; false)

// Use somewhere in the code say at the root component
test({|Dispatch=dispatch; test: 5|})

The memoizeWith callback is not called, and for some reason the function keeps getting called infinitely.

If I change it to a non generic then it works fine

let test =
  FunctionComponent.Of((fun (props: {|Dispatch: Msg -> unit; test: int|}) ->
        log props.test
        span [] []
          ]), memoizeWith= fun o n -> log "haha"; false)

// Use somewhere in the code say at the root component
test({|Dispatch=dispatch; test: 5|})

Not sure if this is an issue with Fable or React.

alfonsogarciacaro commented 5 years ago

Neither, it's an F# thing ;) Generic values are actually compiled as functions and the code is evaluated each time the value is called. If you try the following snippet in F# Interactive, "foo" will be printed two times:

let foo<'T> = printfn "foo"; 5
foo + foo

I acknowledge it's confusing and the need to use generic values if you want to use a generic type in the props makes it unfortunately even more confusing (I also discovered this recently). It's possible to use a generic property if you just use a plain function and instantiate that with ofFunction. But in order to use memoize (which calls React.memo under the hood) you need a value reference, which for generics has the behavior shown above in F#:

let render (props: {| value: 'T |}) = ...

ofFunction render {| value = 5 |} [] // This works

let MyComponent<'T> =
  FunctionComponent.Of((fun (props: {| value: 'T |}) -> ...), memoizeWith=equalsButFunctions)

MyComponent {| value = 5 |} // This creates a new component every time, not the desired behavior

To overcome this, the only solution for now is to create an intermediate function that makes a concrete React component out of the generic function, like this:

let render<'T> (props: {| value: 'T |}) = str ""

let makeComponent<'T>() =
    FunctionComponent.Of(render<'T>, memoizeWith=equalsButFunctions)

let MyIntComponent = makeComponent<int>()

MyIntComponent {| value = 5 |} // The component is memoized as expected