How to get props from children in ReasonReact?


#1

Hi, we are porting a bit of React code to Reason, and ran into this question: How does one get props from children in ReasonReact?

For example this bit of React code iterates over this.props.children, and uses object destructuring to pull out the label out of props, which is used as a tooltip for each Tab:

// React class method
render() {
    const tabElements = Children.toArray(this.props.children);
    return (
      <>
        <TabBar>
          {tabElements.map(({props: {label}}, i) => (
            <Tab
              key={i}
              data-tip={label}  // <----- Here
              data-for={TOOLTIP_ID}
              ...
            >
              ...
            </Tab>
          ))}
          <Tooltip id={TOOLTIP_ID} effect="solid" delayShow={500} place="bottom" />
        </TabBar>
        {tabElements[this.state.selectedTabIndex]}
      </>
    );
  }

When we ported to Reason, the developer had to use bs.raw to extract the props. There are other problems with this Reason code too, like using a string for the pattern match, instead of using a variant type. But the bs.raw usage seems to be really the crux of it:

let getLabel: ReasonReact.reactElement => string =
  [%bs.raw "reactElement => reactElement.props.label"];
let getTabId: ReasonReact.reactElement => string =
  [%bs.raw "reactElement => reactElement.props.tabId"];

[@react.component]
let make = (~children: array(ReasonReact.reactElement)) => {
  let (selectedTabIndex, setSelectedTabIndex) = React.useState(_ => 0);
  <>
    <TabBar>
      (ReasonReact.array(
        children |> Array.mapi((i, tabElement) =>
          <Tab
            key=string_of_int(i)
            dataFor=tabButtonTooltipId
            dataTip=getLabel(tabElement) /* <---- here */
          >
            (
              switch (getTabId(tabElement)) {  /* <---- here */
              | "data" => <OpenIcon height="20px" />
              | "layer" => <LayerIcon height="20px" />
              | "filter" => <FilterIcon height="20px" />
              | "interaction" => <InteractionIcon height="20px" />
              | "map" => <SettingsIcon height="20px" />
              | _ => ReasonReact.null
              }
            )
          </Tab>
        )
      ))
       <Tooltip id=tabButtonTooltipId effect="solid" delayShow=500 place="bottom" />
    </TabBar>
    (Array.get(children, selectedTabIndex))
  </>
};

Any suggestions or guidance appreciated! We are ReasonML newbies. Thanks


#2

@rusticdev I’ll try to answer, but note that I’m also discovering how children work in the new JSX3 so don’t take this as the ultimate solution. :slight_smile:

There is no way to “reach into” children elements in ReasonReact like one can do in ReactJS. The reason is that this reaching is not type safe, one always has to hope that the children passed will fulfill the requirements the component needs (like, having a label in your example).

In ReasonReact, children is just another prop so it can be anything, not only elements. If the component requires a label for each element, we can pass an array of records or an array of tuples with the shape (string, element):

module Tabs = {
  [@react.component]
  let make = (~children) =>
    children
    ->Array.map(((label, element)) =>
        <> <div> {React.string(label)} </div> element </>
      )
    ->React.array;
};

Now, the Tabs component could be used like:

let items = [|
  ("Label 1", <span> "One"->React.string </span>),
  ("Label 2", <span> "Two"->React.string </span>),
|];
ReactDOMRe.renderToElementWithId(<Tabs> items </Tabs>, "preview");

If you somehow needed the label in the elements passed in the array, you would need to

let items = [|
  ("Label 1", <span label="Label 1"> "One"->React.string </span>),
  ("Label 2", <span label="Label 2"> "Two"->React.string </span>),
|];

Or alternatively convert the second part of the tuple into a function:

let items = [|
  ("Label 1", label => <span label> "One"->React.string </span>),
  ("Label 2", label => <span label> "Two"->React.string </span>),
|];

Maybe there are better or simpler options, but this is for now the approach I’m following: use the type system to model more explicitly the data requirements of each component.


#3

@jchavarri thanks so much! This is a really great explanation.