Zaid-Ajaj / Feliz

A fresh retake of the React API in Fable and a collection of high-quality components to build React applications in F#, optimized for happiness
https://zaid-ajaj.github.io/Feliz/
MIT License
531 stars 77 forks source link

Enhance Handling of F# Record Types as Props in ReactComponent Attribute #603

Open lukaszkrzywizna opened 2 months ago

lukaszkrzywizna commented 2 months ago

Issue:

When using Feliz with F# and Fable, passing F# record types as props in React components results in unexpected behavior. The record type, which Fable translates into a JavaScript object with a specific prototype, loses its prototype once processed by React.createElement. This leads to the absence of expected methods and properties on the prototype.

Illustration of simple record transformation:

image

Differences in objects with and without the ReactComponent attribute:

image

Current Workaround by Users:

Currently, a practical workaround for users is to manually wrap the record into an anonymous object. This method involves encapsulating the record as a single property within an anonymous props object, which preserves the prototype through the React component lifecycle.

Example of wrapping the record:

image image

Proposed Solution for Feliz:

I propose that Feliz should either:

  1. Automatically wrap F# record types in an anonymous object when detected as props, thus preserving the prototype integrity.
  2. Implement a warning or error system that alerts developers when a record type is used as props, guiding them towards safer practices or the recommended workaround.

@Zaid-Ajaj @MangelMaxime I'm interested in what you think.

MangelMaxime commented 1 month ago

@lukaszkrzywizna Just to be sure I am reading correctly, in the pictures below the normal functions is without the [<ReactComponent>] and what you would like to have all the time?

Differences in objects with and without the ReactComponent attribute:

image

Proposed Solution for Feliz:

I propose that Feliz should either:

  1. Automatically wrap F# record types in an anonymous object when detected as props, thus preserving the prototype integrity.

I believe this solution would be the best as it requires no knowledge from the user. It would also mean that if F# says this API is available then it will works as expected etc.

However, we need to check if passing a pure Fable record works for react and don't have limitations. For example, will React still be able to diff the properties correctly even if they are 2 levels deep etc. Does this works with record decorated with [<AttachMembers>] which is an extreme case where the methods are attach to the prototype/class instead of being generated as a separated functions.

Regarding [<AttachMembers>], I believe the current implement does not works with it but because this issue is about keeping the prototype it made me think about this special case.

One problem, I could see with this changes is if people use F# to write their component and consume them from JS/TS. Then, they would need to build the properties using the Record constructor exposed by Fable to make sure they have the expected type instance.

lukaszkrzywizna commented 1 month ago

@lukaszkrzywizna Just to be sure I am reading correctly, in the pictures below the normal functions is without the [] and what you would like to have all the time?

A normal function means a function without the [<ReactComponent>] attribute. I would like to maintain the full record prototype chain because erasing it can cause hard-to-identify bugs. In my case, the record was used as a key for a map and it didn’t work due to the lack of that prototype.

However, we need to check if passing a pure Fable record works for react and don't have limitations.

I’ve prepared a new version with automatic wrapping, and so far everything works. It wraps only declared-type records (anonymous records are still processed in the same way). Check the #606.

Does this works with record decorated with [] which is an extreme case where the methods are attach to the prototype/class instead of being generated as a separated functions.

Thanks for the suggestion, I’ll check this.

MangelMaxime commented 1 month ago

Thinking about this issue and PR, there is actually 1 drawbacks in rewritings if the user use a record:

type MyProps =
    {
        value: string
        key: string
    }

Then the code seen by react with be:

createElement(Component, { props = { value: "", key: "" } })

This is problematic because now they component is not anymore optimised by with the key property for the diffing algorithm.

It looks like the current implementation is already making some assumptions on the key property interpretation when it is upper cased.

https://github.com/Zaid-Ajaj/Feliz/blob/4379c03bbe6e3f2438ff1095d0ef9b5f659e032c/Feliz.CompilerPlugins/ReactComponent.fs#L81-L84

I think to work around the issue, I pointed out we should also detect if the record has a key or Key property and if yes duplicate it at the top level too.

createElement(Component, { props = { value: "", key: "" }, key = "" })

This way we can keep both the Record prototype and the key diffing optimisation.

lukaszkrzywizna commented 1 month ago

@MangelMaxime I've added support for lowercased 'key' property.