Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feliz.Router.BasePath stopped navigation #24

Open
SCullman opened this issue Oct 2, 2021 · 17 comments · Fixed by Zaid-Ajaj/Feliz#414
Open

Feliz.Router.BasePath stopped navigation #24

SCullman opened this issue Oct 2, 2021 · 17 comments · Fixed by Zaid-Ajaj/Feliz#414

Comments

@SCullman
Copy link

SCullman commented Oct 2, 2021

After a dotnet paket update I noticed that navigation with Feliz.Router.BasePath stopped working, or better, the hook stopped setting a new state with the current page.

I was able to track it down to Fable.Promise 3.1. When I downgraded to 2.2.2, it worked again.

I still have no idea what is happening, and whether Feliz.Router or the React.useElmish hook is also affected.

@alfonsogarciacaro
Copy link
Member

I guess it's difficult/not possible to provide some code to reproduce. This is strange, the only use of Fable.Promise I can see in all the dependencies of Feliz.Router.BasePath is this call to Promise.start in Feliz.UseElmish. It's true we've have changed the code emitted for Promise.start in 3.1 from $0 to void $0. This should be more correct because the signature of the function returns unit but maybe Feliz.UseElmish depended on this somehow?

@alfonsogarciacaro
Copy link
Member

It would be good to know if other projects using Feliz.UseElmish are having issues after upgrading to Fable.Promise 3.1.

@SCullman
Copy link
Author

SCullman commented Oct 3, 2021

@alfonsogarciacaro, I know my description was not helpful for further investigation. I was already happy to determine Promise as the cause since no error or warning came up during compilation.

@SCullman
Copy link
Author

SCullman commented Oct 3, 2021

"UseElmish" counter works fine with Fable.Promise 3.1

@SCullman
Copy link
Author

SCullman commented Oct 3, 2021

...and Feliz.Router has no dependency on Fable.Promise.

@TWith2Sugars
Copy link

I'm getting the same issue and I think I've narrowed it down to Feliz.UseElmish when the update function returns a state AND another Cmd that isn't Cmd.none. It seems that the returned Cmd isn't processed until something else triggers the update. I'll keep digging to pin it down.

@alfonsogarciacaro
Copy link
Member

Thanks for that @TWith2Sugars! Would you happen to have some code you can share? I could try to compile and see if there's something out of place in the generated JS.

@TWith2Sugars
Copy link

I do but it's part of a rather large private project, let me try and distill it to something smaller.

@TWith2Sugars
Copy link

Right, I've managed to create a repo from the Feliz template and isolate the issue:

https://github.com/TWith2Sugars/FelizPromiseIssue

I've taken the counter example and add an extra Msg IncrementAgain
https://github.com/TWith2Sugars/FelizPromiseIssue/blob/main/src/Main.fs#L10

The update function will increment the state and return a Cmd.ofMsg Increment
https://github.com/TWith2Sugars/FelizPromiseIssue/blob/main/src/Main.fs#L20

When you hit IncrementAgain you'll see the state only increases by 1.

@TWith2Sugars
Copy link

Let me know if there is anything else I can do to help.

@alfonsogarciacaro
Copy link
Member

alfonsogarciacaro commented Oct 6, 2021

Thanks a lot for this @TWith2Sugars! This is super helpful. By testing your repo I realized the reason of the different behaviour was the change in the Run method of the promise CE. If I go back to the previous code, IncrementAgain works.

...However, I'm unsure if going back is the right thing to do for Fable.Promise. Actually after investigating I concluded I was actually using an implementation of Delay/Run that may not be a good fit for JS promises. See #25

TBH, even after spotting the code that's causing the wrong behaviour I still don't know what's actually happening. It's difficult to follow the program flow in UseElmish because it uses multiple React hooks. My intuition is that UseElmish somehow "relied" on a quirk of the previous Fable.Promise implementation. For example, if I replace promise with async here, the problem is still happening:

let rec dispatch (msg: 'Msg) =
    async {
        let mutable nextMsg = Some msg

        while nextMsg.IsSome && not (token.current.IsCancellationRequested) do
            let msg = nextMsg.Value
            let (state', cmd') = update msg state.current
            cmd' |> List.iter (fun sub -> sub dispatch)
            nextMsg <- ring.current.Pop()
            state.current <- state'
            setChildState()
    }
    |> Async.StartImmediate

It'd be ideal if the issue could be solved on the UseElmish side (if we cannot get a failing test for Fable.Promise). I came up with a draft alternative implementation (that's missing things and can definitely be improved) that doesn't use async/promise (as the original Elmish) and it's working:

static member useElmish<'State,'Msg> (init: 'State * Cmd<'Msg>, update: 'Msg -> 'State -> 'State * Cmd<'Msg>, dependencies: obj[]) =
    let exec dispatch cmd =
        cmd |> List.iter (fun call -> call dispatch)

    let setStateRef = React.useRef(ignore)
    let initState, initCmd, dispatch = React.useMemo(fun () ->
        let initState, initCmd = init
        let rb = RingBuffer(10)
        let mutable reentered = false
        let mutable state = initState

        let rec dispatch msg =
            if reentered then
                rb.Push msg
            else
                reentered <- true
                let mutable nextMsg = Some msg

                while Option.isSome nextMsg do
                    let msg = nextMsg.Value
                    let (model', cmd') = update msg state
                    setStateRef.current model'
                    cmd' |> exec dispatch
                    state <- model'
                    nextMsg <- rb.Pop()

                reentered <- false

        initState, initCmd, dispatch            
    )

    let state, setState = React.useState(initState)
    setStateRef.current <- setState

    React.useEffect((fun () ->
        initCmd |> exec dispatch
        React.createDisposable(fun () ->
            getDisposable state
            |> Option.iter (fun o -> o.Dispose())
        )
    ), [||])

    (state, dispatch)

@Shmew @Zaid-Ajaj @MangelMaxime

@MangelMaxime
Copy link
Member

Hello @alfonsogarciacaro,

I am currently working for a client who is using Feliz.UseElmish and was wondering why it is using a custom implementation instead of asking for the standard Program used in Elmish?

My reason is that by not using Program we are losing a lot of stuff like the capability to compose/augment the Program to add new functionality.

Also the Elmish Program has been tested and working since a long time so we would also benefit from that part.

Wouldn't it make sense to try using Elmish Program instead of a custom implementation ?

@TWith2Sugars
Copy link

I've tested those replacements in my larger app and it's resolved all of my issues 👍

@SCullman
Copy link
Author

SCullman commented Oct 6, 2021

@alfonsogarciacaro, this would also fix the initial issue, routing would work again.

@MangelMaxime, I used to work with Elmish Program , but Feliz.useElmish simplifies a lot, if not any of my projects.
Hooks like React.useState() or React.useDeferred() are good enough for navigation and basic initialisation with data. And when a ReactComponent requires more logic, useElmish() still can step in.

@alfonsogarciacaro
Copy link
Member

alfonsogarciacaro commented Oct 6, 2021

I think what @MangelMaxime is saying is we can use React.useElmish but with the original Elmish implementation underneath instead of adding a custom one. And he's probably right 😸 I guess Feliz.UseElmish uses a custom implementation to avoid the chicken-and-egg problem: Program.mkProgram requires you to pass a view function but with useElmish hook you need to initialize Elmish inside the view code. However, there may be a workaround for this.

Following Feliz.UseElmish, I also implemented the hook for Fable.Lit. Then I tried to make it compatible with Elmish Program to take advantage of Elmish add-ons as @MangelMaxime says. But after reading his last comment I tried to use the Elmish program directly and it seems it can be made to work by using an observable-like middle agent: https://github.com/fable-compiler/Fable.Lit/pull/21/files

We can try to do the same for Feliz.UseElmish. The API can be kept the same by building internally the program with the init and update functions (if users want to pass the program directly they can make it with Program.mkHidden init update which is a bit counterintuitive but works).

@TWith2Sugars @SCullman Are you using the alternative useElmish implementation in the comment above? Please note it's not optimized (it retains a reference to the initial state in memory) and it lacks the UseElmish ability to restart the Elmish state when you pass an array of dependencies.

@MangelMaxime
Copy link
Member

@MangelMaxime, I used to work with Elmish Program , but Feliz.useElmish simplifies a lot, if not any of my projects.
Hooks like React.useState() or React.useDeferred() are good enough for navigation and basic initialisation with data. And when a ReactComponent requires more logic, useElmish() still can step in.

@alfonsogarciacaro Did understand what I was saying. I am not saying we should remove Feliz.useElmish but make it accept an Elmish program as an argument.

The idea being that if you want to augment your Elmish loops with the current Feliz.useElmish implementation you can't do it which is a problem. For example, you can't attach a debugger to it neither can you attach a library like Thoth.Elmish.Toast for example.

@TWith2Sugars
Copy link

@TWith2Sugars @SCullman Are you using the alternative useElmish implementation in the comment above? Please note it's not optimized (it retains a reference to the initial state in memory) and it lacks the UseElmish ability to restart the Elmish state when you pass an array of dependencies.

Not in production just as a quick check to see if worked. I've just pinned the Fable.Promise dependency to 2.2.2 for the time being.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants