Help with binding / currying

I’m new to ReScript and struggling a bit with understanding how to do bindings correctly.
Right now, I’m trying to bind the react-hot-toast library, and I have two questions.

First, the library is imported in JS/TS with a named export like so:
import { toast } from 'react-hot-toast'

Then it lets you call toast directly with toast("some text here") but also makes it possible to call it with functions like error toast.error("some err").

Is it possible to use named exports in ReScript? I can only figure out how to do top-level imports. @module("react-hot-toast") compiles to import * as ReactHotToast from.... If I do @module("react-hot-toast/toast") it compile to import * as Toast from...

Question 1
Can I make it compile to import { toast } from 'react-hot-toast somehow?

Right now I have two module mappings to get both available in ReScript.
@module("react-hot-toast") external simple: string => unit = "toast" this lets me call simple("some text here") which is equivilant to the direct call. This works fine and is uncurried in the compiled code.

The other mapping is:

type toastType = {error: (. string) => unit}
@module("react-hot-toast") external show: toastType = "toast"

This compiles to something like ReactHotToast.toast.error("some err");

And it works fine. But if I don’t uncurry the error call, it doesn’t seem to work. Then it compiles to: Curry._1(ReactHotToast.toast.error, "some err"); and then it doesn’t work when I call it.

Question 2
What am I missing to make the “default” curried call work?

I’ve bound to react-hot-toast like this:

module HotToast = {
  type t

  module Toaster = {
    @react.component @module("react-hot-toast")
    external make: unit => React.element = "Toaster"
  }

  @module("react-hot-toast")
  external make: t = "default"

  @send external success: (t, string) => unit = "success"
}

and then use it like:

Lib.HotToast.make->Lib.HotToast.success("Woop!")

Here’s the code for the binding

Hello! Welcome.

Question 1
Currently there’s no way to compile to named import beside default import since semantically:

import * as Toast from 'file'; Toast.error()

fulfills the same purpose as:

import {error} from 'file'; error()

Though arguably the former looks nicer…

However, I have a hunch that this might be what you want:

type myToastType
@module("react-hot-toast") external toast: myToastType = "toast"
@send external error: (myToastType, string) => unit = "error"

error(toast, "some error")

Check the output here.

Question 2

On a higher level: nothing. Because there’s no such thing as curried call in JS. The fact that externals compile to clean uncurried code is a special case actually.

Edit: believer beat me to it. But yes, if you want to also bind to Toaster as a default export, then add that too. Basically, use @send.

2 Likes

Nice, thank you both @believer @chenglou - this was very helpful!

2 Likes

I’ve settled on a slight variation of what you have @believer.

// HotToast.res

type t

module Toaster = {
  @react.component @module("react-hot-toast")
  external make: unit => React.element = "Toaster"
}

%%private(
  @module("react-hot-toast")
  external make: t = "default"

  @module("react-hot-toast") external privateSimple: string => unit = "toast"

  @send external privateSuccess: (t, string) => unit = "success"
  @send external privateError: (t, string) => unit = "error"
)

let simple = msg => privateSimple(msg)
let success = msg => privateSuccess(make, msg)
let error = msg => privateError(make, msg)

This let’s me call it like this HotToast.success("Woop!") and also expose the “default” version with HotToast.simple("Simple Woop!").

Again thanks for your help :pray:t3:

3 Likes

Hah this is what I like. A better answer tailored to the situation. Thanks @believer.

Though be careful not to have too many module wrappings. It gets noisy.

2 Likes

A follow up question. Now I’m trying to bind to the headlessui package from TailwindLabs.

I’m trying to use a named export from their package called Transition.
Normally I would import it like this: import { Transition } from '@headlessui/react' and then just use the Transition component in my markup.

So I’m naively trying this approach:

// HeadlessUI.res

module Transition = {
  @react.component @module("@headlessui/react")
  external make: unit => React.element = "Transition"
}

But when I then use it in my markup like so:

<HeadlessUI.Transition>
   <div>{React.string("hello")}</div>
</HeadlessUI.Transition>

I get an error from the compiler:

The function applied to this argument has type (~?key: string) => {}
This argument cannot be applied with label ~children

So not really sure how to go about fixing this :thinking:

You forgot to define the ~children label for your make function:

module Transition = {
  @module("@headlessui/react") @react.component
  external make: (
    ~show: bool,
    ~enter: string=?,
    ~enterFrom: string=?,
    ~enterTo: string=?,
    ~leave: string=?,
    ~leaveFrom: string=?,
    ~leaveTo: string=?,
    ~children: React.element, // <-- this one
  ) => React.element = "Transition"
}

On another note: I recently used HeadlessUI for rescript-lang.org … here is the relevant diff: https://github.com/rescript-association/rescript-lang.org/pull/301/files#diff-0c189f9d52395944c88299ce525564024d32a5bd0f04f83f68e40dfd58381b1cR1

3 Likes

Wow, thanks. That makes sense!

This got me a long way. Thank you very much!
The next blocker I’m facing is how to map the Transition.Child from the package.
I can see that you don’t use it yet, so there’s not a solution I can steal :grin:

The TransitionChild is not exported so again I’m unsure how to go about this.

My first approach was:

module Transition = {
  @module("@headlessui/react") @react.component
  external make: (
    ~show: bool,
    ~enter: string=?,
    ~enterFrom: string=?,
    ~enterTo: string=?,
    ~leave: string=?,
    ~leaveFrom: string=?,
    ~leaveTo: string=?,
    ~className: string=?,
    ~children: React.element,
  ) => React.element = "Transition"

  module Child = {
    @module("@headlessui/react") @react.component
    external make: (
      ~enter: string=?,
      ~enterFrom: string=?,
      ~enterTo: string=?,
      ~leave: string=?,
      ~leaveFrom: string=?,
      ~leaveTo: string=?,
      ~role: string=?,
      ~onClick: ReactEvent.synthetic<'a> => unit=?,
      ~className: string=?,
      ~style: ReactDOM.Style.t=?,
      ~children: React.element,
    ) => React.element = "Child" // Have also tried "Transition.Child" and "TransitionChild"
  }
}

But I get the error Attempted import error: 'Child' is not exported from '@headlessui/react' (imported as 'React$1').

I’m guessing I need to change the type returned from Transition element. But how do I create a type that will allow Transition itself to be a React.element and also have a Child which is a React.element?

1 Like

Yeah that took me a while as well, since the way headlessUI nests components in their exports looked weird to me.

You need the @scope("Transition") decorator here:

  module Child = {
    @module("@headlessui/react") @scope("Transition") @react.component
    external make: (
      ~enter: string=?,
      ~enterFrom: string=?,
      ~enterTo: string=?,
      ~leave: string=?,
      ~leaveFrom: string=?,
      ~leaveTo: string=?,
      ~role: string=?,
      ~onClick: ReactEvent.synthetic<'a> => unit=?,
      ~className: string=?,
      ~style: ReactDOM.Style.t=?,
      ~children: React.element,
    ) => React.element = "Child" 
  }

Playground Link

4 Likes

Thank you very much. I was wondering what the @scope was for.

I’m so grateful for all the help in this thread. What a welcoming and friendly community :blush:

1 Like

In case you don’t know: You can find all our interop decorators (+ examples) in our syntax discovery widget (feel free to open an issue on the docs issue tracker in case anything you find there is unclear)

We appreciate the questions / threads! Keep it going :ok_hand:

1 Like

Thanks for the link, I didn’t know that :pray:

With my new knowledge of scope I was able to create, at least for me, a cleaner version of the HotToast binding:

// HotToast.res

module Toaster = {
  @react.component @module("react-hot-toast")
  external make: unit => React.element = "Toaster"
}

@module("react-hot-toast") external toast: string => unit = "toast"
@module("react-hot-toast") @scope("toast") external success: string => unit = "success"
@module("react-hot-toast") @scope("toast") external error: string => unit = "error"
5 Likes

Hi @ryyppy in the link to your binding to HeadlessUI you have this thing:

@module("@headlessui/react") @react.component
  external make: (
    ~_as: [#div]=?,
    ~className: string=?,
    ~children: state => React.element,
  ) => React.element = "Menu"

I’m curious as to what the type [#div] is? and how you use it? I can’t seem to figure out where to find more info on this.

Do you know how to type the optional _as to allow for it to receive a React.Fragment?

Finally, have you stopped using HeadlessUI again, if so why?

Thanks in advance :blush:

This is a type annotation for a polymorphic variant.

Usage would look like this: <HeadlessUICom _as=#div> ... </HeadlessUIComp>

Looks like HeadlessUI defines as as a union type (either "div" | ... | React.fragment). ReScript doesn’t support polymorphic values too well, so you need a helper module to unify these types.

Kinda like this:

type state

module As: {
  type t
  let fragment: t
  let dom: [#div] => t
} = {
  type t

  @module("react")
  external fragment: t = "Fragment"

  external dom: [#div] => t = "%identity"
}

module MyComp = {
  @module("@headlessui/react") @react.component
  external make: (
    ~_as: As.t=?,
    ~className: string=?,
    ~children: state => React.element,
  ) => React.element = "Menu"
}

let _ = <MyComp _as=As.fragment> {(_) => <div/>} </MyComp>
let _ = <MyComp _as=As.dom(#div)> {(_) => <div/>} </MyComp>

Playground Link

This is just an example, there are probably cleaner ways to represent this.

Still using it for the upcoming PlaygroundWidget. Not planning on switching, except I am reimplementing the whole thing in pure Rescript.

Hope this helps!

2 Likes

Incredible, thanks for such a detailed example.
This was very helpful :+1: :+1: :+1:

Hmm, nobody seems to have binding for Headless Dialog…I keep getting this:

Uncaught Error: You have to provide an open and an onClose prop to the Dialog component.

This is my binding:

@react.component @module("@headlessui/react")
  external make: (
    ~children: React.element=?,
    ~_as: string,
    ~className: string=?,
    ~onClose: bool => unit,
    ~_open: bool,
  ) => React.element = "Dialog"

Any idea why? Or is there a complete set of HeadlessUI bindings somewhere :slight_smile:

The best way to debug this is to look at the JS output. Did your program actually compile and yield the JS output you’d expect? Does it look exactly like in the HeadlessUI docs and still doesn’t work?

1 Like

here is my binding for headless Dialog

module Dialog = {
  @module("@headlessui/react") @react.component
  external make: (
    ~\"as": string,
    ~children: React.element,
    ~className: string=?,
    ~onClose: 'a => unit,
    ~static: bool=?,
    ~\"open": bool,
  ) => React.element = "Dialog"

  module Overlay = {
    @module("@headlessui/react") @scope("Dialog") @react.component
    external make: (~className: string=?) => React.element = "Overlay"
  }
  module Title = {
    @module("@headlessui/react") @scope("Dialog") @react.component
    external make: (~className: string=?, ~children: React.element) => React.element = "Title"
  }
}

2 Likes