How do I perform HTTP requests

Thank you, @spyder.
I’ll look into that library.

  1. We discourage the former, and most likely the latter until it matures.
  2. It’s about the paradigm. Promise doesn’t have cancellation and is an inextensible API. Lemme repeat that network programming is about properly handling different failure conditions (among which is proper cancellation and cleanup), not about happy path programming.

Promises are always wrong in React. See https://gist.github.com/chenglou/b6cf738a5d7adbde2ee008eb93117b49 and the first comment in the gist.

4 Likes

If you have a larger app and want to avoid boilerplate you can also look into a GraphQL client. All data-fetching ceremony is abstracted away and you just describe data requirements in a declarative way.

3 Likes

:bulb: Seems like HTTP requests over XMLHTTPRequest deserves it’s own section in the ReScript docs. Would be great to see more examples on how to do HTTP POST with body and http headers, as well as a graphql example.

13 Likes

I just wonder of how using this for multiple http request? what I got is when one request is began to load, the other is cancelled.

What I’m doing is create request on every react component’s useEffect. I guess by creating request on every component should fix, but it’s not.

React.useEffect0(() => {
let request = makeXMLHttpRequest()
request->addEventListener("load", () => {
  let response = request->response->parseResponse
  setHomepage(_ => {message: response["message"]})
})
request->addEventListener("error", () => {
  Js.log("Error logging here")
})
request->open_("GET", url.homepage)

request->send

None
})

Edited: Ok I figured out how, I just need to make external binding on every component too.

1 Like

make sure to return a cleanup callback for your useEffect, bc that’s the whole point of using XmlHttpRequest.

Like here

2 Likes

Have there been developments in upstreaming this to the stdlib? I can’t find anything about this in the rescript docs. I don’t mean to sound demanding btw, I’m just curious since this was mentioned earlier last year. It’s a bit tedious to use the snippet to avoid promises (labeled as the wrong approach) since there’s a lot of boilerplate to do a simple GET.

2 Likes

I see some people complaining about boilerplate here, so I thought I’d share my HTTP Client for general feedback (or as an example for anyone that’s interested). It has:

  • ability to do a get request (still need to add post etc)
  • ability to unauthenticate a user or refresh auth token
  • comes with a provider component and a hook
// HTTPClient.res

open Http

type successCB = (Js.Json.t, status)=> unit
type errorCB = unit => unit
type cleanup = unit => unit

type t = {
  get: (~relativeURL: string, ~successCB: successCB, ~errorCB: errorCB) => cleanup,
  // post: (~relativeURL: string, ~successCB: successCB, ~errorCB: errorCB, ~body: Js.Json.t) => unit,
}

let newAuthenicatedClient = (~baseURL, ~unauthenticate, ~getAuthToken) => {
  let getTokenCB = cb => {
    open Promise
    getAuthToken()
    ->Promise.then(token => {
      cb(token)
      resolve()
    })
    ->catch(_ => {
      unauthenticate()
      resolve()
    })
    ->ignore
  }

  let get = (~relativeURL, ~successCB, ~errorCB) => {
    let request = makeXMLHttpRequest()
    request->addEventListener("load", () => {
      let status = request->statusCode->codeToStatus
      let response = request->response->toJson

      switch status {
      | StatusUnauthorized => unauthenticate()
      | _ => successCB(response, status)
      }
    })

    request->Http.addEventListener("error", () => {
      errorCB()
    })

    request->Http.open_("GET", baseURL ++ relativeURL)

    getTokenCB(token => {
      request->Http.setHeader("Authorization", "Bearer " ++ token)
      request->Http.send
    })

    () => request->abort
 }

  {
    get: get,
  }
}

let context = React.createContext(
  newAuthenicatedClient(
    ~baseURL="",
    ~unauthenticate=() => (),
    ~getAuthToken=() => Promise.resolve(""),
  ),
)

module Provider = {
  let provider = React.Context.provider(context)

  @react.component
  let make = (~client: t, ~children) => {
    React.createElement(provider, {"value": client, "children": children})
  }
}

let use = () => React.useContext(context)

It’s a lot, but the usage is quite nice (and most people can probably cut out a lot of the auth logic).

Usage:

Wrap the client somewhere at the top of your component tree (I’m using Auth0 btw, just if anyone is wondering where my token logic is coming from):

let client = HttpClient.newAuthenicatedClient(
      ~baseURL="http://localhost:4000",
      ~unauthenticate=() => logout({returnTo: Window.Location.origin}),
      ~getAuthToken=() => {
        open Promise
        getIdTokenClaims()->Promise.then(tokenClaims => resolve(tokenClaims.__raw))
      },
    )

<HttpClient.Provider client> children </HttpClient.Provider>

Then you can easily access the client anywhere in your component tree with the Hook:

let client = HttpClient.use()

Then you can inject it into some data layer, or run it in useEffect to cleanup on component unmount. You can do a GET request as follows:

React.useEffect0(() => {
     let cleanup = client.get(
      ~relativeURL="/some/relative/path",
      ~successCB=(json, status) => {
        Js.log(json)
      },
      ~errorCB=() => Js.log("error"),
    )

    Some(() => cleanup())
} 

I’d love some feedback, specifically around the Promise logic as that’s also new to me. Hope this is useful :slight_smile:

Lastly, I do have some added utils in my Http bindings, I’ll just dump the whole module here

// Http.res
type request
type response
type status =
  | StatusOk //200
  | StatusCreated //201
  | StatusNoContent //204
  | StatusBadRequest //400
  | StatusUnauthorized //401 unauthenticated
  | StatusForbidden //403 unauthorized
  | StatusNotFound //404
  | StatusInternalServerError //500

let codeToStatus = statusInt =>
  switch statusInt {
  | 200 => StatusOk
  | 201 => StatusCreated
  | 204 => StatusNoContent
  | 400 => StatusBadRequest
  | 401 => StatusUnauthorized
  | 403 => StatusForbidden
  | 404 => StatusNotFound
  | _ => StatusInternalServerError
  }

@new external makeXMLHttpRequest: unit => request = "XMLHttpRequest"
@send external addEventListener: (request, string, unit => unit) => unit = "addEventListener"
@get external response: request => response = "response"
@get external statusCode: request => int = "status"
@send external open_: (request, string, string) => unit = "open"
@send external setHeader: (request, string, string) => unit = "setRequestHeader"
@send external send: request => unit = "send"
@send external abort: request => unit = "abort"

@scope("JSON") @val
external toJson: response => Js.Json.t = "parse"
2 Likes

That’s kinda harsh, we have a way to treat unhappy paths with Promise.catch (I’m talking about Promise api in general) as stated above, fetch today has an option of cancelation in the most modern browsers (AbortController & AbortSignal | Can I use... Support tables for HTML5, CSS3, etc). I don’t think that chunk of code being repeated everywhere (or every project having to write its own wrapper) is a good solution, from Rescript own’s site: “Ever wanted a language like JavaScript, but without the warts…”, I think this kinda goes against all Rescript’s nicely thought syntax.

Pardon me the extreme, but imagine if webdev community had thought that jquery had a terrible API and have decided go back to use DOM directly.

2 Likes