fable-compiler / Fable.Lit

Write Fable Elmish apps with Lit
https://fable.io/Fable.Lit/
MIT License
91 stars 13 forks source link

What does this mean? #42

Closed leolorenzoluis closed 2 years ago

leolorenzoluis commented 2 years ago

Given the following code that is just rendering a list of items

let value, setValue = Hook.useState item
let inputRef = Hook.useRef<HTMLInputElement>()

html $"""
<div>
    <input value={value.Id} />
    <input type="text" {Lit.refValue inputRef} value={value.FriendlyName} @keyup={EvVal (fun e -> printfn "Hello %A" e; setValue { value with FriendlyName = e })}/>
    <input value={value.URL}/>
    <sl-button @click={Ev (fun e -> SaveMonitor value |> dispatch)}>Save monitor</sl-button>
</div>
"""

Error: Hooks must be called consistently for each render call at HookContext.fail (Hook.fs:95:9) at HookContext.checkRendering (Hook.fs:126:32) at HookContext.useState (Hook.fs:171:9)

alfonsogarciacaro commented 2 years ago

Hmm, this message should be displayed when hooks are not called consistently. Do you have a conditional or similar in the function that wraps the Hook.useState/useRef calls? Can I see the whole function? Is it decorated with HookComponent/LitElement or is it a helper function? Is it inlined? Thanks!

leolorenzoluis commented 2 years ago

Yes I do. I have refactored it a little bit trying to pull from your TodoMVC 👯 with just scoped css but I get same error.


[<HookComponent>]
let Page() =
    let model, dispatch = Hook.useElmish(init, update)
    let renderItem (item: Monitor) =
        match model.Edit with
        | Some value when value.Id = item.Id->
            html $"""
            <div>
                <p>{item.Id}</p>
                <input value={item.FriendlyName} @keyup={EvVal (fun e -> UpdateFriendlyName e |> dispatch)}/>
                <input value={item.URL}/>
                <sl-button @click={Ev (fun e -> SaveMonitor model.Edit.Value |> dispatch)}>Save monitor</sl-button>
            </div>
            """
        | _ ->
            let transitionMs = 500
            let transition = Hook.useTransition(transitionMs, onLeft = (fun () -> DeleteMonitor item.Id |> dispatch))
            let className = Hook.use_scoped_css $"""
                :host {{
                    transition-duration: {transitionMs}ms;
                    border: 2px solid lightgray;
                    border-radius: 10px;
                    margin: 5px 0;
                }}
                :host.transition-enter {{
                    opacity: 0;
                    transform: scale(2);
                }}
                :host.transition-leave {{
                    opacity: 0;
                    transform: scale(0.1);
                }}
                .is-clickable {{
                    user-select: none;
                }}
            """
            html $"""
            <div class="{className} {transition.className}">
                <p>{item.Id} {item.FriendlyName} {item.URL}</p>
                <sl-button @click={Ev (fun e -> DeleteMonitor item.Id |> dispatch)}>Delete monitor</sl-button>
                <sl-button @click={Ev (fun e -> EditMonitor item |> dispatch)}>Edit monitor</sl-button>
            </div>
            """

    html $"""
     <sl-button @click={Ev (fun e ->
            let newMonitor = {
                Id = Random.Shared.Next() 
                FriendlyName = "Test1"
                URL = "2"
                Retries = 100
                HeartBeatInterval = 100
            }
            CreateMonitor newMonitor |> dispatch)}>New monitor</sl-button>
        {model.Monitors |> Seq.map renderItem}
    """

Maybe I'm doing something I shouldn't like mixing useElmish with useState, etc...?

alfonsogarciacaro commented 2 years ago

Just as with React, hooks should appear on top of the components and not be affected by the control flow. In your case renderItem is being called for each item, so the following lines are called multiple times:

let transition = Hook.useTransition(transitionMs, onLeft = (fun () -> DeleteMonitor item.Id |> dispatch))
let className = Hook.use_scoped_css $"""

The solution would be to extract that code into a separate component (below is a sample, you would have to pass the dispatch function to make it compilable:


[<HookComponent>]
let Monitor(item: Monitor) =
    let transitionMs = 500
    let transition = Hook.useTransition(transitionMs, onLeft = (fun () -> DeleteMonitor item.Id |> dispatch))
    let className = Hook.use_scoped_css $"""
        :host {{
            transition-duration: {transitionMs}ms;
            border: 2px solid lightgray;
            border-radius: 10px;
            margin: 5px 0;
        }}
        :host.transition-enter {{
            opacity: 0;
            transform: scale(2);
        }}
        :host.transition-leave {{
            opacity: 0;
            transform: scale(0.1);
        }}
        .is-clickable {{
            user-select: none;
        }}
    """
    html $"""
    <div class="{className} {transition.className}">
        <p>{item.Id} {item.FriendlyName} {item.URL}</p>
        <sl-button @click={Ev (fun e -> DeleteMonitor item.Id |> dispatch)}>Delete monitor</sl-button>
        <sl-button @click={Ev (fun e -> EditMonitor item |> dispatch)}>Edit monitor</sl-button>
    </div>
    """

[<HookComponent>]
let Page() =
    let model, dispatch = Hook.useElmish(init, update)
    let renderItem (item: Monitor) =
        match model.Edit with
        | Some value when value.Id = item.Id->
            html $"""
            <div>
                <p>{item.Id}</p>
                <input value={item.FriendlyName} @keyup={EvVal (fun e -> UpdateFriendlyName e |> dispatch)}/>
                <input value={item.URL}/>
                <sl-button @click={Ev (fun e -> SaveMonitor model.Edit.Value |> dispatch)}>Save monitor</sl-button>
            </div>
            """
        | _ -> Monitor item

    html $"""
     <sl-button @click={Ev (fun e ->
            let newMonitor = {
                Id = Random.Shared.Next() 
                FriendlyName = "Test1"
                URL = "2"
                Retries = 100
                HeartBeatInterval = 100
            }
            CreateMonitor newMonitor |> dispatch)}>New monitor</sl-button>
        {model.Monitors |> Seq.map renderItem}
    """
leolorenzoluis commented 2 years ago

Awesome. Thank you @alfonsogarciacaro!