DEV Community

Cover image for Buckle with React sub-component
shayan
shayan

Posted on

Buckle with React sub-component

React (also known as React.js or ReactJS) is an open-source JavaScript library for building user interfaces. React can be used as a base in the development of single-page or mobile applications. Regarding the React pattern, it brings simplicity and maintainability. Almost all projects I used, like SPA projects with React, or Next.js, and even ReactNative, I used these techniques to reduce code duplication, readability, and maintainability.

What exactly is a sub-component?

Sub-components mean combining a group component in one component, By using sub-components we can render the same view, but with a much more readable code and a reusable component. Sub-component can reduce a significant amount of code duplication and make your code so simple to read and understand.

Why do I need to use a sub-component in my project?

Of course, I believe that knowledge is a vital asset for every developer, but still we cannot find a use case, we barely use them during our daily development. As a result, let's find out why and when we need to use sub-component. To make it clear let's say we are going to implement a component to provide us Bootstrap card modules. So, let's first define what a Bootstrap card component contains. Well, a Bootstrap card component contains 3 parts, a header, a body, and a footer.

bootstrap card parts

So if we suppose to build this component, it would be so simple,

//components/card.js
import React from 'react';

const Card = ({ cover_image, children, footer }) => (
  <div className="card">
    <img src={cover_image} className="card-img-top" />
    <div className="card-body">
      {children}
    </div>
    <div className="card-footer">
      <small className="text-muted">{footer}</small>
    </div>
  </div>
);

export default Card;

And you can use it in an application like this

<Card
  cover_image="https://dummyimage.com/400x120/dedede/000&text=cover image"
  footer="Last updated 3 mins ago"
>
  <h5 className="card-title">Card title</h5>
  <p className="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This card has even longer content than the first to show that equal height action.</p>
</Card>

But things get a bit hard when we need to add this feature in a way that footer and header get a DOM element, besides an image URL and text. So what can we do in that situation? Maybe one of the easiest ways is to pass a DOM element to footer and cover image or pass them another component, so our code would be like this

//components/card.js
const Card = ({ header = ‘’, children, footer = ‘’ }) => (
  <div className="card">
    {header}
    <div className="card-body">
      {children}
    </div>
    <div className="card-footer">
      {footer}
    </div>
  </div>
);


// App.js

<Card
  header={<img src="https://dummyimage.com/400x120/dedede/000&text=cover image" class="card-img-top" />}
footer={<div className="card-footer"><small class="text-muted">Last updated 3 mins ago</small></div>}
>
  <h5 className="card-title">Card title</h5>
  <p className="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This card has even longer content than the first to show that equal height action.</p>
</Card>

Yep, works fine. But just to consider it as clean and readable, I think it would be a bit hard to read or maintain it if the footer and header element grows. In conclusion, here is exactly where we need a sub-component, we can rewrite the above component as below, which is more readable and of course clean. Means, besides passing footer and header to the component, we can pass them as the children within the tag of Card.Header and Card.Footer.

<Card>
  <Card.Header>
    <img src="https://dummyimage.com/400x120/dedede/000&text=cover image" class="card-img-top" />
  </Card.Header>
  <Card.Body>
    <h5 className="card-title">Card title</h5>
    <p className="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This card has even longer content than the first to show that equal height action.</p>
  </Card.Body>
  <Card.Footer>
    <small class="text-muted">Last updated 3 mins ago</small>
  </Card.Footer>
</Card>

How to build a sub-component?

So let’s go add a sub-component step by step to our Card component. As it’s obvious, we somehow should build the Card.Header, Card.Body, and Card.Footer and assign them to the Card component, then we should get them in the Card component and fill the component with their data. As React is a strong library, we can assign some parameter to the component, like below

import React from 'react';

const CustomComponent = ({...}) => (...);

CustomComponent.displayName = 'custom-component';
CustomComponent.SubComponent = AnotherComponent;

export default CustomComponent;

Dadaaaa, the secrets are revealed. So we should build Header, Footer, and Body as a component then assign them to the Card component, this way, they would be properties of our Card component. But how can we get them in the Card component so that we are able to render them in different parts? Don’t worry we will discuss this later, then stay tuned.

Firstly, let’s add Header, Body, and Footer components to the Card component. But before it, let's practice a thing together, let's say we have an object name MyObject which has a property named foo.

const MyObject = {
   foo: bar
};

console.log(MyObject.foo); // bar

We also can add another property to this object, and use it later.

const MyObject = {
   foo: bar
};

MyObject.new_prop = hello world;

console.log(MyObject.new_prop); // bar

Dadaaaa, this is the approach we are going to do to add those above components to our Card component.
So we can define our sub-component and then assign them to the Card component, just like below.

import React from 'react';

const Card = ({ header = '', children, footer = '' }) => (
...
);

const Header = ({ children }) => children;
Card.Header = Header;

const Body = ({ children }) => children;
Card.Body = Body;

const Footer = ({ children }) => children;
Card.Footer = Footer;

export default Card;

Just like the above example, now, we have access to Card.Header, Card.Body, and Card.Footer. So we can rewrite our code as below

<Card>
  <Card.Header>
    <img src="https://dummyimage.com/400x120/dedede/000&text=cover image" class="card-img-top" />
  </Card.Header>
  <Card.Body>
    <h5 className="card-title">Card title</h5>
    <p className="card-text">This is a wider card with supporting text below as a natural lead-in to additional content. This card has even longer content than the first to show that equal height action.</p>
  </Card.Body>
  <Card.Footer>
    <small class="text-muted">Last updated 3 mins ago</small>
  </Card.Footer>
</Card>

As we use our Card sub-component within the Card tag <Card>...</Card>, they are detected as children for Card components, so we don’t have direct access to them, to get them as a prop and use them, wherever needed, for that important, we need to make a trick. We can assign a name to our sub-components by displayName prop, then filter the children element of the Card component to find them. So we can implement it as below

const Header = ({ children }) => children;
Header.displayName = 'Header';
Card.Header = Header;

const Body = ({ children }) => children;
Body.displayName = 'Body';
Card.Body = Body;

const Footer = ({ children }) => children;
Footer.displayName = 'Footer';
Card.Footer = Footer;

And then our Card component would be like this.

import React from 'react';

const Card = ({ children }) => {
  const header = React.Children.map(children, child => child.type.displayName === 'Header' ? child : null);
  const body = React.Children.map(children, child => child.type.displayName === 'Body' ? child : null);
  const footer = React.Children.map(children, child => child.type.displayName === 'Footer' ? child : null);
  return (
    <div className="card">
      {header}
      <div className="card-body">
        {body}
      </div>
      <div className="card-footer">
        {footer}
      </div>
    </div>
  );
}

const Header = ({ children }) => children;
Header.displayName = 'Header';
Card.Header = Header;

const Body = ({ children }) => children;
Body.displayName = 'Body';
Card.Body = Body;

const Footer = ({ children }) => children;
Footer.displayName = 'Footer';
Card.Footer = Footer;
export default Card;

You can find the whole code here in this repository

Join the discussion

I would love to get some feedback here.

Top comments (2)

Collapse
 
saadanimohamedamine profile image
Saadani Mohamed Amine

Thanks for the explanation !!

Collapse
 
lallenfrancisl profile image
Allen Francis

Been searching for this. I couldn't find a sane explanation in the docs