rescriptbr/react-query

queryFn signature does not model fallibility for 'queryError

Closed this issue · 2 comments

Problem

My queryFn returned promise.t has a result<'a, 'e>, but I 'queryError is completely disassociated.

queryFn: ReactQuery_Types.queryFunctionContext<'queryKey, 'pageParam> => Js.Promise.t<'queryData>

Discussion

As rescript users denote, Js.Promise is more or less worthless for modelling error types, which turns out is pretty critical for JS applications.

I'm currently using aantron/promise which at runtime is the practical mapping between this library's 'queryError and my 'e of result<'a, 'e>. The same could be said for ryyppy's or yawaramin's promise impl's as well.

I'm curious if there's a way to abstract the types here to satisfy the status quo, but also play nicely with result aware promise impls that care about modelling the error case. perhaps a GADT on the return type could be applicable and processed accordingly in the the useQuery impl?

I was able to do this mapping within my own domain.

external unsafeToJsExn: exn => Js.Exn.t = "%identity"

type jsexn_or_err<'t> =
  | JsError(Js.Exn.t)
  | Err('t)

type rec queryResultR<'queryError, 'queryData> = {
  status: ReactQuery_Types.queryStatus,
  // ..snip
  error: option<jsexn_or_err<'queryError>>,
}

// useQuery wrapper for doing all Promise modeling with Result types
let useQueryR = (~queryFn) => {
  let (err, setErr) = React.useState(_ => None)
  let queryFnWithResultHandling = React.useCallback1(opts => {
    queryFn(opts)
    ->Promise.tapError(e => setErr(_ => Some(Err(e))))
    ->Promise.Js.fromResult
    ->Promise.Js.toBsPromise
      |> Js.Promise.catch(exn => {
        let err = exn->%raw("(x => x instanceof Error ? x : new Error(String(x)))")
        setErr(_ => Some(JsError(err)))
        Js.Promise.reject(err)
      })
  }, [queryFn])
  let res = ReactQuery_Query.useQuery(
    ReactQuery_Query.queryOptions(~queryFn=queryFnWithResultHandling, ()),
  )
  {
    status: res.status,
    // ...snip
    error: err,
  }
}

// Demo!

type foo_err = Zed
let foo = () => {
  if true {
    Ok(1)
  } else {
    Error(Zed)
  }
}

let v = useQueryR(~queryFn=_ => Promise.resolved(foo()))

switch v.error {
| Some(JsError(e')) => () // e' ~ Js.Exn.t
| Some(Err(e')) => () // e' ~ foo_err
| _ => ()
}->ignore
// v.data //  option<int>

@cdaringe Sorry for the delay. I was about to suggest you to do something like that. This package is focused on (almost) zero-cost bindings but I understand that the usage of react-query with ReScript could be better if we use built-in structures like variants.
However, I prefer to support zero-cost bindings and provide you a way to create your own custom wrappers instead to make this package a very opinionated bindings that doesn't provide the possibility to create a wrappers or custom hooks based on react-query.