Marshall Bowers

Conjurer of code. Devourer of art. Pursuer of æsthetics.

React Associated Components

Saturday, July 10, 2021
272 words
2 minute read

When building React components I often find myself in situations where there are groups of components that regularly appear together. For example, consider this Card component that can be used with header, body, and footer components:

import React from 'react';
import { Card } from './Card';
import { CardBody } from './CardBody';
import { CardFooter } from './CardFooter';
import { CardHeader } from './CardHeader';

const App = () => (
  <Card>
    <CardHeader>A Movie Listing (2021)</CardHeader>
    <CardBody>This movie is awesome!</CardBody>
    <CardFooter>
      <Button>Watch Movie</Button>
    </CardFooter>
  </Card>
);

It can be rather cumbersome to have to import all of the various components every time we want to use them, especially since every time we use a Card we'll want to use one or more of these components with it. Likewise, these components won't be used outside the context of a Card.

Instead, we can make CardHeader, CardBody, and CardFooter associated components1 on the Card:

import React from 'react';
import { Card } from './Card';

const App = () => (
  <Card>
    <Card.Header>A Movie Listing (2021)</Card.Header>
    <Card.Body>This movie is awesome!</Card.Body>
    <Card.Footer>
      <Button>Watch Movie</Button>
    </Card.Footer>
  </Card>
);

Here's what things look like under the hood:

import React from 'react';
import { CardBody } from './CardBody';
import { CardFooter } from './CardFooter';
import { CardHeader } from './CardHeader';

export interface CardProps
  extends React.DetailedHTMLProps<
    React.HTMLAttributes<HTMLDivElement>,
    HTMLDivElement
  > {}

export interface Card {
  Header: typeof CardHeader;
  Body: typeof CardBody;
  Footer: typeof CardFooter;
}

export const Card: React.FC<Readonly<CardProps>> & Card = ({
  children,
  ...props
}) => (
  // The most basic of `Card`s.
  <div {...props}>{children}</div>
);

Card.Header = CardHeader;
Card.Body = CardBody;
Card.Footer = CardFooter;

And that's all there is to it!

1

This name is inspired by Rust's associated types.