Return component from hook / bind to component with ref to div

Hey, I ran into some use cases in the last while for binding to interesting React libraries and was hoping to get some help.

Example

For simplicity, I’ll use one example that covers both use-cases I want to ask about (basically the most complicated example I’ve run into). Rive React has a hook, useRive which returns, amongst other things, a component and an object needed to control animations:

let { RiveComponent, rive } = useRive(params);

<div>
  <button onClick={_ => rive.play()}>"play"</button>
  <RiveComponent/>
</div>

Questions

  • Is it possible to replicate returning the RiveComponent in Rescript? (I’m thinking not) The alternative would be to bind to the RiveJS library and recreate all the logic that they maintain in their useRive hook?
  • The component returned has a ref to a <div> component, how does one translate that in the binding?

Thanks in advance for any help :slight_smile:

Here’s a quick draft to get you started:

module Rive = {
  type t

  @send external play: t => unit = "play"

  type hookResult = {
    @as("RiveComponent") riveComponent: React.component<unit>,
    rive: t,
  }

  @module("rive-react")
  external useRive: {"src": string, "autoplay": bool} => hookResult = "useRive"
}

module Example = {
  @react.component
  let make = () => {
    let {riveComponent, rive} = Rive.useRive({
      "src": "loader.riv",
      "autoplay": false,
    })

    // We can't just use <riveComponent/> ... we need to use the createElement api directly
    let riveElement = React.createElement(riveComponent, ())

    <div>
      <button onClick={_ => rive->Rive.play}> {React.string("play")} </button>
      riveElement
    </div>
  }
}

Playground Link

Regarding your first question: You are correct; it’s not possible to just use RiveComponent like that in ReScript due to the limitations of upper / lower-cased identifier names. You can still handle the component function though and instantiate it with React.createElement.

Note: I am not familiar with Rive, nor anything related to rive, so these bindings might be inefficient, or even wrong. Always check the JS output when designing bindings.

1 Like

Of course, before I get called out: Technically that’s not fully true. There is a way to express the component as a so-called “First Class Module” (basically handling a module like if it was a value during runtime).

The full example would look like this:


module Rive = {
  module type RiveComponent = {
    @react.component
    let make: () => React.element
  }
  
  type t

  @send external play: t => unit = "play"

  type hookResult = {
    @as("RiveComponent") component: module(RiveComponent),
    rive: t,
  }

  @module("rive-react")
  external useRive: {"src": string, "autoplay": bool} => hookResult = "useRive"
}

module Example = {
  @react.component
  let make = () => {
    let {component, rive} = Rive.useRive({
      "src": "loader.riv",
      "autoplay": false,
    })

    // We `unpack` the module into a proper runtime value, so we can use it in our JSX
    module RiveComponent = unpack(component)

    <div>
      <button onClick={_ => rive->Rive.play}> {React.string("play")} </button>
      <RiveComponent/>
    </div>
  }
}

Playground Link

First Class modules are an advanced concept we didn’t document yet, because users tent to overthink it and reach for the big guns whenever they are hitting a simple problem. Both ways are kinda valid, the FCM case does probably make more sense on the usage site, but introduces more concepts.

2 Likes

That’s incredible :exploding_head: promise I won’t use this too much!

Am I missing something with regards to my second question about the ref to div? I don’t see anything that covers that usecase, does that get done on the RiveComponent type? :slight_smile:

Can you give an example on how the ref would look like in plain JS? I didn’t see it in your initial post, and thought you were talking about the rive.play()?

Apologies I see that I didn’t make it very clear. The RiveComponent that is returned has all the attributes of the div component. Docs:

This component accepts the same attributes and event handlers as a div element. 

This is a common use-case, eg. a component library that has a Button component with all the attributes of the <button> tag. I actually made an assumption that the way to do this would be by using a React.ref, I’m not sure how this would translate to bindings at all :slight_smile:

JS example:

<RiveComponent 
  className="py-2"
  onMouseEnter={() => console.log("hello rescript")}
/>

I realize that the easy way to do this would be to just type up all the params that you want to use:

module type RiveComponent = {
    @react.component
    let make: (~className: option<string>=?, ~onMouseEnter ...) => React.element
  }

So this is more a question out of interest :slight_smile:

Can’t think of a better way right now than the thing you just proposed.

Ideally you’d need to create a @obj external makeProps function that covers all attributes of the ReactDOM.domProps type, but without the ref.

This use-case popped up more often lately, so I will check back with @rickyvetter to see if we can streamline this somehow.

1 Like

@ryyppy so your first suggestion works perfectly, which transpiles as:

return React.createElement(match.RiveComponent, undefined);

But when using FCM it transpiles to:

return React.createElement(match.RiveComponent.make, {});

Which makes sense because that’s exactly what we’re telling it to do, but RiveComponent.make is undefined. I guess this is because I’m not actually binding to an external React component, but rather receiving one that doesn’t have a make function defined? Printing out the actual component (RiveComponent) returned from useRive gives me:

ƒ (e){return n.createElement(u,i({setContainerRef:m,setCanvasRef:R},e))}

Not sure how to take this further, but I can at least get by with the first solution, thanks! :slight_smile: