Complex bindings with the new [@react.component]?

interop

#1

Hi everyone, I am unsure if this is the good place to ask, or if I should go to stack overflow instead, but I have a question about the new JSX3 / react components interop:

The documentation states that “using a component written in JS requires a single external to annotate the types it takes”. But the example provided is most simple: a component with a single string.

How should we deal with more complex props that need some custom conversion work ? Before I would have written something like that:

[@bs.deriving jsConverter]
 type labelPos = [
   | [@bs.as "left"] `left
   | [@bs.as "right"] `right
 ];

[@bs.module "semantic-ui-react"]
external react : ReasonReact.reactClass = "Input";

[@bs.obj] 
external makeProps :
  (
    ~labelPosition: string,
    unit
  ) =>  _ = "";

let make =
    (
      ~labelPosition,
      children,
    ) =>
  ReasonReact.wrapJsForReason(
    ~reactClass=react,
    ~props=
      makeProps(
        ~labelPosition=labelPosToJs(labelPosition),
        (),
      ),
    children,
  );

The new way would look like:

[@bs.module "semantic-ui-react"] [@react.component]
external make : (labelPosition: string) => React.element = "Input";

But is there a way to provide a custom serializer for props using the new JSX3 interop ?


#2

You can just use an ordinary function where you do the transformation before calling the external. Something like this:

[@bs.module "semantic-ui-react"]
external make : (~labelPosition: string) => React.element = "Input";

[@react.component]
let make = (~labelPosition) =>
  make(~labelPosition=labelPosToJs(labelPosition));

#3

Thank you for that quick answer ! I indeed tried something similar, but forgot to move the [@react.component]annotation to my new make function… It works now and is still less overhead than the old wrapJsForReason technique.


#4

Sorry to bring up an old thread, but it’s one of the few results on google on this topic.

I tried @glennsl’s example but bucklescript seems to be generating some strange code.

Here’s my simple example:

[@bs.deriving jsConverter]
type card = {
  selected: bool,
  title: string,
  text: string,
};

[@bs.module "./AttributeCard"]
external make:
  (
    ~card: {
             .
             "selected": bool,
             "title": string,
             "text": string,
           },
    ~saved: bool
  ) =>
  React.element =
  "default";

[@react.component]
let make = (~card: card, ~saved) => make(~card=cardToJs(card), ~saved);

When used, this generates the following JS:

import * as AttributeCard from "./AttributeCard";

function cardToJs(param) {
  return {
          selected: param[/* selected */0],
          title: param[/* title */1],
          text: param[/* text */2]
        };
}

function AttributeCard(Props) {
  var card = Props.card;
  var saved = Props.saved;
  return AttributeCard.default(cardToJs(card), saved);
}

var make = AttributeCard;

I would have expected the code generated to call AttributeCard.default with a JS object rather than as parameters. I’m using [@genType.import "./AttributeCard"] to workaround this but it feels wrong.

Am I missing something?

Thanks :slight_smile:


#5

@lozlow I’m not sure how the original solution proposed in Complex bindings with the new [@react.component]? works, and haven’t used genType either. I usually approach this problem with a combination of makeProps and make.

To take your example, it would look like this:

[@bs.deriving jsConverter]
type card = {
  selected: bool,
  title: string,
  text: string,
};

type props = {
  .
  "card": {
    .
    "selected": bool,
    "title": string,
    "text": string
  },
  "saved": bool
};

let makeProps = (~card: card, ~saved: bool, ()): props => {
  {
    "card": cardToJs(card),
    "saved": saved
  }
};
  
[@bs.module "./AttributeCard"]
external make: React.component(props) = "default";

More in the docs: https://reasonml.github.io/reason-react/docs/en/components#hand-writing-components


Trouble Importing From JS
#6

Thanks for the reply @pewniak747, that worked out great!

I had tried to follow that part in the docs before but your code sample made it much clearer thanks!

One last thing, where you have:

let makeProps = (~card: card, ~saved: bool, ()): props => {

Why is a unit () as the last parameter necessary here?
If I omit it I get the error:

Error: This function has type
...
It is applied to too many arguments; maybe you forgot a `;'.

I thought () was only required when one or more of the parameters was optional or provided a default value?


#7

I thought () was only required when one or more of the parameters was optional or provided a default value?

Right, that’s normally the case for functions where the optional argument is the last one.

However, the final unit () is always required for your custom react makeProps functions. This is due to how the JSX transforms into make and makeProps function invocations. Thanks to this, any component can potentially have an optional prop (a common one is key).

Check out the JSX examples at https://reasonml.github.io/reason-react/docs/en/jsx#capitalized