2

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>
 )
1

1 Answer 1

4

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);
Sign up to request clarification or add additional context in comments.

4 Comments

What if you wanted the consumer to pass specifically a Header, so that they cannot pass something like an Accordion?
I see were you're going with your alternate solutions. However, I have many use cases for which I need an answer to the question instead of other approaches. Essentially, the child.type === Component check
I've edited in some more ideas. But inspecting the types and props of children and then acting on those is not something i recommend doing unless you absolutely need to. React's design makes it easy for a parent to tell a child what to render, but the reverse is tricky.
Based from this information, I'll conclude that the pattern itself of using type === Component is fundamentally flawed. Thanks for your answer Nicholas!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.