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

ReactComponentAttribute for props list #581

Open lukaszkrzywizna opened 10 months ago

lukaszkrzywizna commented 10 months ago

Hi, First of all, I love Feliz! I very much like the paradigm it offers, especially the IReactProperty list approach. It's very readable and closest to the original JS/JSX approach.

However, when I use it in conjunction with ReactComponent attribute I see one drawback - created react component instead of containing an object with props taken from the list, it has one single prop of a list type. In most cases it doesn't matter, but based on react-memo article text:

When a component visually wraps other components, let it accept JSX as children. This way, when the wrapper component updates its own state, React knows that its children don’t need to re-render.

we can assume that react somehow by checking what kind of props are passed.

Additionally, using traditional object param is more convenient when using React developer tools: For code


[<ReactComponent>]
let Tst1 (props: IReactProperty seq) =
    Html.h1 [
        prop.style [ style.color.red ]
        yield! props
    ]

[<ReactComponent>]
let Tst2 (text: string, ariaLabel: string) =
    Html.h1 [
        prop.style [ style.color.red ]
        prop.text text
        prop.ariaLabel ariaLabel
    ]
image

versus

image

The easiest solution is to manually translate prop-list into an object. However, I wonder if we could do something in the scope of Feliz.CompilerPlugin.

I imagine that by using a special attribute (ReactComponent or something new), the plugin could detect if a function expects a single prop of a list type and map the input into an object by calling Object.fromEntries and within the function unwrap it back by using Object.entries. #580 is a small POC that illustrates the solution:

[<ReactListComponent>]
let Tst2 (props: IReactProperty seq) =
    Html.h1 [
        prop.style [ style.color.red ]
        yield! props
    ]

Tst2 [ prop.text "My text" ]

gives something similar to this

export function Tst2(tst2InputProps) {
    const props = (($value) => Object.entries($value))(tst2InputProps);
    return createElement("h1", createObj(toList(delay(() => append(singleton(["style", {
        color: "#FF0000",
    }]), delay(() => props))))));
}

createElement(Tst2, (($value) => Object.fromEntries($value))([["children", "Some text"]]))

We can go further, and extend the existing tuple-component approach by allowing to put an extra list parameter that would be merged with the rest of the explicitly provided params (not implemented):

[<ReactListComponent>]
let Tst2 (className: string, props: IReactProperty seq) =
    Html.h1 [
        prop.style [ style.color.red ]
        prop.className className
        yield! props
    ]
Tst2 ("my-class", [ prop.text "Some text"])

export function Tst2({ className, ...tst2InputProps }) {
    const props = (($value) => Object.entries($value))(tst2InputProps);
    return createElement("h1", createObj(toList(delay(() => append(singleton(["style", {
        color: "#FF0000",
    }]), delay(() => append(singleton(["className", className]), delay(() => props))))))));
}

createElement(Tst2, {
        className: "my-class",
        ...((($value) => Object.fromEntries($value))([["children", "Some text"]])),
    })

@Zaid-Ajaj I'd like to know your opinion about that. If you think that this makes sense, I can continue developing the solution. I'm open to any other option/approach.

Zaid-Ajaj commented 9 months ago

Hi there @lukaszkrzywizna sorry for the delayed response 🙏 been busy with day job, hard to keep track of everything going on in the OSS space.

About IReactProperty list I really think of this as purely an interop mechanism when binding third-party components. If you are building your own components in pure F#, use normal parameters with [<ReactComponent>].

Using [<ReactListProperty>] does sound interesting for niche use cases, in general I believe [<ReactComponent>] should do the trick but i agree that ReactListProperty is more flexible. To be honest I am a bit reluctant on adding more ways to implement these React components but I will think about it more

lukaszkrzywizna commented 9 months ago

No problem @Zaid-Ajaj I know you're busy man :) I'll try to investigate it further in a free time and share something more.