Fred
Fred

Reputation: 43

How to access Fable's promise results

Just starting out with Fable / Elmish / F#, but getting stuck getting a basic concept done...

The goal of the code is to execute a simple HTTP GET request and, thereafter, to post the results of the GET request to the user on a web page. The idea was to use Fable Promise or async to execute the GET request and return the result, thereafter the result can be displayed on a web page using Elmish and Fable.

I expected that the web page (localhost) will output a text string containing the response of of the GET request, instead I only got "[object Promise]" on the web page. The desired response of the GET request is however logged on the Browser console. If I issue a printfn command with the txt in the body of the promise (before the return) I can also see the result of the GET request in the browser console.

Thus: I have no idea how to access the results of the promise. Please help, what am I missing? The sample apps from [Fable][1] , there is a image "container" created and an image placed based on the results of the HTTP get, but I need to work with text (json string).

Here is my code:

module App

open Elmish
open Fable.React
open Elmish.React
open Fable.React.Props
open Fable.Import
open Fable.PowerPack

open Fetch

// CallAPI
let getMe = promise { 
                    let! res = fetch "https://reqres.in/api/users/2" [] 
                    let! txt = res.text()              
                    // Access your resource here
                    Browser.console.log txt 
                    return txt
                    }

//State
type MyInputString = 
     {Response : string}


// Message
type Message = 
           |ShowResponse
           |Error

let getFromAPI  = 
     getMe |> sprintf "%A" 


//Update
let update msg model = 
           match msg with 
           |ShowResponse -> {Response = getFromAPI }
           |Error -> {Response = "Errrrrr"}


//Helper function
let text(content:string) : ReactElement = unbox content
//Helper function
let init() = {Response = ""}


//View
let view model dispatch = 
    div
     []
      [
          div [] [ input[ Type "text"] ]
          button [ OnClick (fun _ -> dispatch ShowResponse ) ] [ text "Show Me"]
          div [] []
          div [] [text (model.Response)]
      ]

Program.mkSimple init update view
|> Program.withReactSynchronous "werk"
|> Program.withConsoleTrace
|> Program.run ```




  [1]: https://fable.io

Upvotes: 4

Views: 1690

Answers (2)

Maximilian Wilson
Maximilian Wilson

Reputation: 461

Promises are a fundamentally imperative construct--you can only get the value out of them using side effects. They aren't like a Task in C# which can also yield a value synchronously. That means your model MUST have two states related to getMe: the state it's in while waiting for getMe to finish, and the state it's in when getMe completes. Each state will have its own message and its own case in update. The handler for the first case will begin getMe using Cmd.ofSub, and will ensure that when the promise completes, it has the side effect of sending the second message, which will show the result of the promise in the UI.

So in addition to ShowResponse and Error, you need a third message MakeRequest. Code below is just a sketch (not tested) but illustrates the point:

// Message
type Message = 
  | MakeRequest
  | ShowResponse of string
  | Error

let getMe dispatch = 
   promise { 
     let! res = fetch "https://reqres.in/api/users/2" [] 
     let! txt = res.text()              
     // Access your resource here
     Browser.console.log txt 
     dispatch (ShowResponse txt)
   }

//Update
let update msg model = 
  match msg with 
  | MakeRequest -> { model with Response = "Wait..." }, 
                     Cmd.ofSub(fun dispatch -> getMe dispatch |> Promise.start)
  | ShowResponse msg -> { model with Response = msg }, Cmd.empty
  | Error -> { model with Response = "Errrrrr"}, Cmd.empty

Upvotes: 4

onemorecupofcoffee
onemorecupofcoffee

Reputation: 2447

If you do it following the elmish pattern, you could do the following. This might give some errors but should be enough to get you going:

Use mkProgram because it gives you flexibility to specify Cmd callbacks

Program.mkProgram init update view
    |> Program.withReact "elmish-app"
    |> Program.withConsoleTrace
    |> Program.run

Make sure you have a distinction between your model and message

type Model = {
    UserMessage: string
    ErrorMessage: string
}

Add a message type to receive the promise result and to handle an error:

type Message = 
        | Loading
        | ShowResponse of Result<string,string>
        | Error of exn

Your init should return model and Cmd now as the signature has changed (see update below):

let init() = { UserMessage= "" ; ErrorMessage= "" } , Cmd.ofMsg Loading

Change your update function, the signature has now changed as we are using mkProgram. Note that when you call Cmd.ofPromise you are passing the ShowResponse and Error messages as callbacks -ShowResponse on success and Error if anything bad happens in your promise code.

//update signature has changed from
msg -> model -> model
//to:
msg -> model -> (model*Cmd<msg>)

let update msg model = 
            match msg with 
            | Loading -> 
                { model with UserMessage= "Loading" }, 

                    Cmd.ofPromise (getMe ) () ShowResponse Error
            | ShowResponse  resp -> 
                match resp with
                    | Ok str -> 
                        { model with UserMessage = str }, Cmd.none
                    | Error str -> 
                        { model with ErrorMessage = str }, Cmd.none                     
            | Error exn ->  { model with ErrorMessage = string exn }, Cmd.none

Upvotes: 6

Related Questions