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
540 stars 78 forks source link

Stopping asynchronous recursive updates when useElmish component destroyed #471

Closed sWW26 closed 2 years ago

sWW26 commented 2 years ago

I have an application where a component using useElmish is polling the server while a condition is true, in a similar fashion to the Tick example in https://zaid-ajaj.github.io/the-elmish-book/#/chapters/commands/async-recursive-updates. The problem I'm having is that when I leave the page contianing said component the dispatch loop continues to run causing unneccessary API calls.

What's the best approach to stop messages from being processed inside the useElmish dispatch loop once the component is destroyed? I see if state implements IDisposable it will be called but I'm not sure how this could be used to clean up outstanding recursive messages.

MangelMaxime commented 2 years ago

Hello @sWW26, in theory, if your component is not loaded anymore in the DOM the Elmish application is destroyed and so will not continue generating messages.

When you say "leave the page" is the Elmish component removed from the DOM or just hidden by something else?

sWW26 commented 2 years ago

That's correct the component is removed, however the update function is still processing messages then raising new commands through Cmd.ofAsync. I'll try and put together a simple example.

sWW26 commented 2 years ago

I've made the smallest example I could think of to show my issue, click the "Start polling API" button once and it'll start printing to the console once per second. If you then click the "Navigate" button FirstComponent is removed but you'll see the console messages continue.

open System
open Feliz
open Feliz.UseElmish
open Elmish
open Browser.Dom

type Msg =
    | PollApi

let init () =
    (), Cmd.none

let update msg () =
    match msg with
    | PollApi ->
        let pollApi () =
            printfn $"Making API call at {DateTime.Now}"
            Async.Sleep 1000
        () , Cmd.OfAsync.perform pollApi () (fun () -> PollApi)

[<ReactComponent>]
let FirstComponent navigateAway =
    let state, dispatch = React.useElmish(init, update, [||])
    Html.div [
        Html.div "FirstComponent"
        Html.button [
            prop.text "Start polling API"
            prop.onClick (fun _ -> dispatch PollApi)
        ]
        Html.button [
            prop.text "Navigate"
            prop.onClick (fun _ -> navigateAway())
        ]
    ]

[<ReactComponent>]
let SecondComponent() =
    Html.div "SecondComponent"

[<ReactComponent>]
let App() =
    let hasNavigatedAway, updateHasNavigatedAway = React.useState(false)
    if hasNavigatedAway then SecondComponent() else FirstComponent (fun () -> updateHasNavigatedAway true)

ReactDOM.render(App(), document.getElementById "app")
sWW26 commented 2 years ago

Achieved what I needed by: