Let's say I have a higher-order component, something like the following trivial definition, exported from the JavaScript module ./hoc.js
:
export const withStrong =
Component => props =>
<strong> <Component ...props/> </strong>
Assuming I have some component called HelloMessage
, what is the equivalent of this piece of JavaScript:
import { withStrong } from './hoc.js';
const HelloMessage = ...
const StrongMessage = withStrong(HelloMessage);
ReactDOM.render(
<StrongMessage name="Joe" />,
document.getElementById('react-app')
);
TL;DR:
This should be the exact equivalent of the requested JavaScript snippet:
[@bs.module ./hoc.js]
external withStrong
: React.component('props) => React.component('props)
= "withStrong";
module HelloMessage = ...
module StrongMessage = {
include HelloMessage;
let make = withStrong(make);
};
ReactDOMRe.renderToElementWithId(
<StrongMessage name="Joe" />,
"react-app"
);
There'a also a runnable example on the Reason playground with a few adaptations made to work around not having a separate JavaScript file.
Explanation follows:
withStrong
is just a function. It happens to be a function that accepts and returns a react component, which is a bit mysterious, but they're really just values like any other. We can just bind it like an ordinary function.
Even something as simple as this would work
[@bs.module ./hoc.js]
external withStrong : 'a => 'a = "withStrong";
assuming you always make sure to pass in a component. But it wouldn't be particularly safe as you can pass it anything else too, so let's try to use the type system as it should be used, restricting it to only accept react components.
The ReasonReact source code says components have the type component('props)
, so that's what we'll use.
[@bs.module ./hoc.js]
external withStrong
: React.component('props) => React.component('props)
= "withStrong";
Using the 'props
type variable in both the argument and return type means we constrain them to be the same. That is, the returned component will have exactly the same props as the one passed in, which is exactly what we want in this case.
And that's really all there is to the binding itself. we can now use it like this:
let strongMessage = withStrong(HelloMessage.make);
Unfortunately this doesn't support JSX. To render strongMessage
as is we'd have to write something like
React.createElementVariadic(strongMessage, { "name": "Joe" }, [||]);
Not great. So let's fix that.
<StrongMessage name="Joe" />
transforms to
React.createElementVariadic(
StrongMessage.make,
StrongMessage.makeProps(~name="Joe", ()),
[||]
);
So we need a StrongMessage
module with two functions, make
and makeProps
that conform to what's expected by React.createElementVariadic
. make
is just the component itself, so that's simple enough. makeProps
is a function that acccepts the props as labeled arguments terminated by unit
(since the props may be optional) and returns a js object. This also happens to be exactly what [@bs.obj]
does, which isn't in any way coincidental.
Putting this together then, we get:
module StrongMessage = {
let make = withStrong(HelloMessage.make);
[@bs.obj]
external makeProps
: (~name: string, unit) => {. "name" string }
= "";
}
And that's it! Yay!
Addendum: Shortcuts
Ok, so the makeProps
function is a bit of an annoying mouthful. Fortunately in our case, where the props of the wrapped component is the same as the original, it's also unnecessary since StrongMessage.makeProps
will be identical to HelloMessage.makeProps
. Let's just steal that then! And now we have
module StrongMessage = {
let make = withStrong(HelloMessage.make);
let makeProps = HelloMessage.makeProps;
}
But we can do even better! By using include HelloMessage
we can drop makeProps
entirely (thanks to @bloodyowl, via @idkjs, for this one).
module StrongMessage = {
include HelloMessage;
let make = withStrong(make);
}
That's pretty nice, isn't it? This works because include HelloMessage
will include all the exported definitions from HelloMessage
such as makeProps
, but also make
and anything else. This is probably what you want when you wrap a component in this way, but beware that it imports and re-exports everything from the included module, in case that's not what you want.
Finally, once we have both the binding and JSX in order we can use it like this
ReactDOMRe.renderToElementWithId(
<StrongMessage name="Joe" />,
"react-app"
);