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.

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:

2 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

2 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?

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

1 Like

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:

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