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?
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>
}
}
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.
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>
}
}
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.
That’s incredible 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?
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
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: