How to pass a component constructor as a prop

In React something I often do when creating a component library is something like this:

type Props = {
  icon: JSXElementConstructor<{ className?: string }>
}

const SomeComponent = ({ icon: Icon }) => {
  return <div>An icon: <Icon className="set-by-parent" />;
};

...

import { Check } from "some-icon-library";

<SomeComponent icon={Check} />

With the idea being that the user can pass in any component constructor and all the right props will be set automatically by the parent.

I’m having some trouble figuring out how to do this in ReScript (or if it’s possible). I realize the types are stricter so instead of accepting any constructor that takes className as a prop, I’m trying to set up a component that takes more a more specific set of props. This is what I have so far:

// Lucide.res
module Props = {
  type t = {
    size?: int,
    color?: string,
    strokeWidth?: int,
    absoluteStrokeWidth?: bool,
    className?: string,
  }
}

module Check = {
  @module("lucide-react") @react.component(: Props.t)
  external make: unit => React.element = "Check"
}

And what I want to do is something like this:

// IconButton.res
@react.component
let make (~icon) => {
  <button>
    <icon size={16} color="white" /> // This is the prop
  </button>
}

This feels like it should be possible, but maybe I’m mistaken. Thanks!

Yes! It is possible with first-class modules:

type iconProps = {
  size?: int,
  color?: string,
  strokeWidth?: int,
  absoluteStrokeWidth?: bool,
  className?: string,
}

module type Icon = {
  let make: React.component<iconProps>
}

module Check: Icon = {
  @module("lucide-react")
  external make: React.component<iconProps> = "Check"
}

module IconButton = {
  @react.component
  let make = (~icon: module(Icon)) => {
    module Icon = unpack(icon)

    <button>
      <Icon size={16} color="white" />
    </button>
  }
}

let _ = <IconButton icon={module(Check)} />

Playground link

It’s one of the features that still lacks documentation: Document First Class Modules · Issue #155 · rescript-lang/rescript-lang.org · GitHub

EDIT: Updated it to a full example.

4 Likes

Amazing, thank you so much! I’ll definitely need to look more into first class modules to understand how this actually works. But I was able to get optional icons working based off the code you provided!

@react.component
let make = (~variant=Variant.Success, ~label="", ~icon: option<module(Lucide.Icon)>=?) => {
  let icon = switch icon {
  | Some(i) => {
      module Icon = unpack(i)
      <Icon size={16} />
    }
  | None => React.null
  }

  <div>
    {icon}
    {React.string(label)}
  </div>
}

Edit: An even simpler version

module Empty = {
  @react.component(: Lucide.Props.t)
  let make = () => {
    React.null
  }
}

@react.component
let make = (
  ~variant=Variant.Success,
  ~label="",
  ~icon: option<module(Lucide.Icon)>=module(Lucide.Empty),
) => {
  module Icon = unpack(icon)

  <div>
    <Icon size={16} />
    {React.string(label)}
  </div>
}
1 Like

Ah yes that is shorter. I typically just use render functions which are most of the time sufficient for my needs. I think this is the first time I see an FCM as a default parameter.

We usually don’t advise to use them since they are quite syntax-heavy and make prototyping harder as you cannot infer types from usage but must declare a module type up front.

What do you mean by “I typically just use render functions which are most of the time sufficient for my needs?”

Also if there is a more idiomatic ReScript way to solve this problem I’d love to know, I could definitely be too narrow minded trying to do it the way I do in JS/TS.

Something like this:

type iconProps = {
  size?: int,
  color?: string,
  strokeWidth?: int,
  absoluteStrokeWidth?: bool,
  className?: string,
}

module Check = {
  @module("lucide-react")
  external make: React.component<iconProps> = "Check"
}

module IconButton = {
  @react.component
  let make = (~label="", ~renderIcon: iconProps => React.element) =>
    <div>
      {renderIcon({size: 16, color: "white"})}
      {React.string(label)}
    </div>
}

let _ = <IconButton renderIcon=Check.make />
// or, alternatively
let _ = <IconButton renderIcon={props => <Check {...props} />} />

But you lose the nice JSX syntax.

2 Likes

That seems super obvious in retrospect! I think I prefer it too, and I don’t mind losing the JSX. Thanks!