Christopher Francisco
Christopher Francisco

Reputation: 16278

Can I have Component interfaces in React?

Say I have a <Modal> that takes a <Header> <Content> and <Footer>.

(
  <Modal>
    <Header>Foo</Header>
    <Content>Foo</Content>
    <Footer>Foo</Footer>
  </Modal>
)

Now, inside my Modal component I'll probably have code like the following:

const header = children.find(child => child.type === Header)

In order to get a reference to the rendered header.

Now, what if from the consumer of the modal, I needed a decorated Header. Let's just call it DecoratedHeader

// DecoratedHeader
const DecoratedHeader = () => <Header>Foo <Icon type="lock" /></Header>

// consumer
(
  <Modal>
    <DecoratedHeader />
    <Content>Foo</Content>
    <Footer>Foo</Footer>
  </Modal>
)

The line above wouldn't work anymore, as DecoratedHeader type is not Header. However, it IS rendering a Header.

It feels like there's the concept of "interface" which is missing. Ultimately, the Modal cares for a Header to be rendered, but if you wrap it under a "custom" component there's no way for it to know that it is still a Header.

What am I missing?

EDIT

To expand more about my use cases, I don't need an alternative solution. I need to know whether React has support for a mechanism equivalent to an interface, where 2 different Components that comply with the Liskov Substitution Principle (meaning they're swappable) can have a way to be picked by the parent.

Specifically, replacing this "hardcoded implementation" search, with an "interface" search:

-const specificChild = children.find(child => child.type === SomeComponent)
+const componentInterface = children.find(child => ????)

 // Get a prop out of that component interface
 const { someInterfaceProp } = componentInterface.props;

 return (
   <div>
     {componentInterface}  {/* render it on a specific place */}
   </div>
 )

Upvotes: 2

Views: 257

Answers (1)

Nicholas Tower
Nicholas Tower

Reputation: 84982

Assuming the only thing you're going to be doing with these components is rendering them in specific spots of the modal, i would do them as separate props. For example:

const Modal = ({ header, content, footer }) => {
  return (
    <div>
      {header}
      <SomethingElseAllModalsHave />
      {content}
      {footer}
    </div>
  )
}

// ... used like:
const Example = () => {
  return (
    <Modal
      header={<DecoratedHeader />}
      content={<Content>Foo</Content>}
      footer={<Footer>Foo</Footer>}
    />
  )
}

If you need the modal to not just render the other components, but give them some information too, you could use a render prop. Basically the same as my example above, but now you pass in functions instead of elements

const Modal = ({ header, content, footer }) => {
  const [isVisible, setIsVisible] = useState(false);

  return (
    <div>
      {header(isVisible)}
      <SomethingElseAllModalsHave />
      {content(isVisible)}
      {footer(isVisible}
    </div>
  )
}

// ... used like:
const Example = () => {
  return (
    <Modal
      header={() => <DecoratedHeader />}
      content={(isVisible) => <Content>{isVisible ? "Foo" : "Bar"</Content>}
      footer={(isVisible) => isVisible ? <Footer>Foo</Footer> : null}
    />
  )
}

EDIT:

When you write the JSX <DecoratedHeader/>, the object that is produced contains no information about <Header>. It's basically just an object with a type (ie, a reference to DecoratedHeader) and some props (none in this case). Header only enters the picture when DecoratedHeader is rendered, which won't be until after Modal is rendered.

So whatever the characteristics are that Modal will use to identify what is and is not a header, it needs to be something that is on DecoratedHeader, not just on Header. Perhaps you could add a static property to any component that counts as a header, and then check for that:

const Header = () => {
  // Whatever the code is for this component.
}

Header.isHeader = true;

const DecoratedHeader = () => <Header>Foo <Icon type="lock" /></Header>

DecoratedHeader.isHeader = true;

Then you'll look for it something like this (you should use React.Children, because children is not guaranteed to be an array):

const header = React.Children.toArray(children).find(child => child.type.isHeader);

Upvotes: 4

Related Questions