Skip to content

[JSX] Prevent making the whole children list dynamic when having a for loop in a list of children #4079

Closed

Description

@MangelMaxime

Hello,

Consider the following DSL (this is Feliz adapted to use Fable JSX module)

open Fable.Core
open Fable.Core.JsInterop

[<ImportMember("react")>]
[<AllowNullLiteral>]
type ReactElement = interface end

type private IReactProperty = JSX.Prop

[<Erase>]
type prop =
    static member inline children(children: ReactElement list) : IReactProperty = "children", children

    static member inline text(value: int) : IReactProperty = "children", [ JSX.text !!value ]

    static member inline key(value: int) : IReactProperty = "key", string value

[<Erase>]
type Html =

    static member inline fragment(children: ReactElement list) : ReactElement =
        JSX.create "" [ "children" ==> children ] |> unbox

    static member inline div(number: int) : ReactElement =
        JSX.create "div" [ "children" ==> JSX.text (!!number) ] |> unbox

    static member inline div(children: ReactElement list) : ReactElement =
        JSX.create "div" [ "children" ==> children ] |> unbox

    static member inline div(text: string) : ReactElement =
        JSX.create "div" [ "children" ==> JSX.text text ] |> unbox

    static member inline div(props: IReactProperty list) : ReactElement = JSX.create "div" props |> unbox
[<Erase; Mangle(false)>]
type Test =

    [<ExportDefault>]
    static member Test() =
        Html.div
            [
                Html.div "Test 1"
                Html.div "Test 2"
            ]
export function Test() {
    return <div>
        <div>
            Test 1
        </div>
        <div>
            Test 2
        </div>
    </div>;
}

export default Test;

which is fine as all the children are "static".

However if we do, we add a for loop, this transform the whole children list into a dynamic list.

[<Erase; Mangle(false)>]
type Test =

    [<ExportDefault>]
    static member Test() =
        Html.div
            [
                Html.div "Test 1"
                Html.div "Test 2"
                for i in 0..2 do
                    Html.div [
                        prop.key i
                        prop.text i
                    ]
            ]
export function Test() {
    return <div>
        {toList(delay(() => append(singleton(<div>
            Test 1
        </div>), delay(() => append(singleton(<div>
            Test 2
        </div>), delay(() => map((i) => <div key={int32ToString(i)}>
            {i}
        </div>, rangeDouble(0, 1, 2))))))))}
    </div>;
}

export default Test;

This is problematic because now React will complain that not all child have a key prop.

We can work around this issue by wrapping the list in React.fragment, as it scope the dynamic list to only the for loop scope.

[<Erase; Mangle(false)>]
type Test =

    [<ExportDefault>]
    static member Test() =
        Html.div
            [
                Html.div "Test 1"
                Html.div "Test 2"
                Html.fragment
                    [
                        for i in 0..2 do
                            Html.div [ 
                                prop.key i
                                prop.text i 
                            ]
                    ]
            ]
export function Test() {
    return <div>
        <div>
            Test 1
        </div>
        <div>
            Test 2
        </div>
        <>
            {toList(delay(() => map((i) => <div key={int32ToString(i)}>
                {i}
            </div>, rangeDouble(0, 1, 2))))}
        </>
    </div>;
}

export default Test;

However, this is a standard pattern in JSX and would be nice to be supported out of the box.

Candidate for the output would be something like:

export function Test() {
    return <div>
        <div>
            Test 1
        </div>
        <div>
            Test 2
        </div>
        {toList(delay(() => map((i) => <div key={int32ToString(i)}>
                {i}
            </div>, rangeDouble(1, 1, 10))))}
    </div>;
}

I think it should be doable, by detecting the toList call and extracting places where we see delay(() => append(singleton(...

Issue originally reported by @Freymaurer

Activity

  1. changed the title [-][JSX] Prevent make the whole children list dynamic when having a for loop in a list of children[/-] [+][JSX] Prevent making the whole children list dynamic when having a for loop in a list of children[/+] on Mar 15, 2025
  2. MangelMaxime commented on Mar 16, 2025

    @MangelMaxime
    MemberAuthor

    I think we will also need to wrap / convert the toList into a native JavaScript array as toList will create a F# list.

  3. MangelMaxime commented on Mar 17, 2025

    @MangelMaxime
    MemberAuthor

    After some testing it seems like we can keep toList as is at least this works for react.

    The reason why it works, is probably because FSharpList implements the GetEnumerator / [Symbol.iterator]

  4. shayanhabibi commented on Mar 18, 2025

    @shayanhabibi
    Contributor

    I ran into this when dealing with JSX outputs as this was preventing attributes in Solid-js from working when using simple conditional statements, my fix was to just aggressively unroll the default Fable AST 😅
    Since Solid-js doesn't permit loops/map in children as it is unreactive and provides the For and Index primitive components instead, I didn't have to do it inside the children blocks.

    What exactly is delay?

  5. MangelMaxime commented on Mar 18, 2025

    @MangelMaxime
    MemberAuthor

    @shayanhabibi Thanks for the hint, I was doing something similar in Fable.

    Having your implementation will probably save me some time and trouble 😇

  6. MangelMaxime commented on Apr 17, 2025

    @MangelMaxime
    MemberAuthor

    @shayanhabibi Thanks a lot for your initial implementation of the Unroller.

    I would not have though of doing it this way, and it really helped me get a clean implementation I think.

  7. MangelMaxime commented on Apr 17, 2025

    @MangelMaxime
    MemberAuthor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions