Search code examples
f#fable-f#elmish

Return async value from Fable.Remoting


Here is a client side Fable.Remoting example that prints the result of an async function.

    // Client code (Compiled to Javascript using Fable)
    // ============
    open Fable.Remoting.Client

    let server = Proxy.create<IServer>

    async {
     let! length = server.getLength “hello”
     do printfn “%d” length // 5
    }
    |> Async.StartImmediate

How do I get the length value?


Solution

  • I see you've tagged your question with , so I'm going to assume you have a Msg type defined. Don't use Async.StartImmediate or Async.RunSynchronously; in Elmish, you should use Cmd.OfAsync to schedule a message to be dispatched once the async block returns a value. There are four functions in Cmd.OfAsync (and the same four appear in Cmd.OfPromise as well): either, perform, attempt, and result. I'll break them down for you since their documentation isn't quite up to snuff yet:

    • either: takes four parameters, task, arg, ofSuccess, and ofError. task is the async function you want to call (of type 'a -> Async<'b>). arg is the parameter of type 'a that you want to pass to the task function. ofSuccess is a function of type 'b -> 'Msg: it will receive the result of the async function and is supposed to create a message, presumably one that incorporates the 'b result. Finally, ofError is a function of type exn -> 'Msg: if the task function throws an exception, then ofError will be called instead of ofSuccess, and is supposed to turn that exception into an Elmish message that your code can handle (presumably one that will log an error to the Javascript console or pop up a notification with Thoth.Toast or something like that).
    • perform: like either but there's no ofError parameter. Use this if your async command cannot fail (which is never the case with remote API calls, as it's always possible the network is down or your server is unresponsive), or if you just don't care about exceptions and don't mind an unhandled exception getting thrown.
    • attempt: like either but there's no ofSuccess parameter, so the task function's result will be ignored if it succeeds.
    • result: this one is completely different. It just takes a single parameter of type Async<'Msg>, i.e. you pass it an async block that is already going to produce a message.

    With the code you've written, you would use Cmd.OfAsync.result if you wanted to make minimal changes to your code, but I would suggest using Cmd.OfAsync.perform instead (and upgrading it to Cmd.OfAsync.either once you have written some error-handling code). I'll show you both ways:

    type Msg =
        // ... rest of your messages go here
        | GetLength of string
        | LengthResult of int
    
    let update msg model =
        match msg with
        // ... rest of your update function
        | GetLength s ->
            let usePerform = true
            if usePerform then
                model, Cmd.OfAsync.perform server.getLength s LengthResult
            else
                let length : Async<Msg> = async {
                    let! length = server.getLength s
                    return (LengthResult length)
                }
                model, Cmd.OfAsync.result length
        | LengthResult len ->
            // Do whatever you needed to do with the API result
            printfn "Length was %d" len
            model, Cmd.none
    

    And if you were using either (which you really should do once you go to production), there would be a third message LogError of exn that would be handled like:

        | LogError e ->
            printfn "Error: %s" e.Message
            model, Cmd.none
    

    and the Cmd.OfAsync.perform line in the code above would become:

            model, Cmd.OfAsync.either server.getLength s LengthResult LogError
    

    That's the right way to handle async-producing functions in Elmish.