DEV Community

Liam
Liam

Posted on

Abstracting Seasonal Components

It could be said that one of the core purposes of software engineering is Abstraction. Reducing complexities and making reusable software empowers us to work with higher-level concepts. Why spend time editing 3 files when you can just edit one?

Reusable components are the cornerstone of UI development. It is consistent, faster, and it makes maintenance much easier.

To make something reusable, we need to identify variable and constant elements. Constants stay inside the base component, while variables are exposed as parameters (props) so each instance can customize them.

In my case, I have three Hero sections on a website - Fall, Winter, and Summer - that share the same structure and style but differ in content:

Property Type in Abstraction Notes
"Real Farm Fun" Tagline Variable (currently constant) Same across all heroes now, but could change in future. Pass as prop to keep flexible.
"Celebrate fall…" Description Variable (currently constant) Same now, but keep configurable for special events.
Backdrop image Variable Each hero uses a different image.
Call-to-action Variable Different sets of buttons and descriptions.
Season info Variable Shows “we’re open” or other seasonal info.
Styling & structure Constant Shared layout, typography, and positioning are fixed in the base component.

Given this, the prop contract for the reusable hero component includes:

  • Backdrop Image: Image source (and optional blur placeholder)
  • Call-to-action:
    • An array of buttons
      • Labels, href, and icons
    • CTA description
  • Season Info
    • Title
    • Content
  • Tagline & Description: Passed as props to allow flexibility, even if often the same

The base component (AbstractHero) implements the shared structure and styling. Seasonal heroes provide only the variable content.

Flowchart

The Code

Before:

export default function HeroSummer() {
  return (
    <section className={styles.hero}>
      <Image
        src="/summer.webp"
        width={1500}
        height={1000}
        priority
        placeholder="blur"
        blurDataURL="/summer-xs.webp"
        alt="Old McDonald's"
      />
      <div className={styles.cover}>
        <div className={styles.top}>
          <h1 className={styles.tagline}>Real Farm Fun</h1>
          <div>
            <p className={styles.description}>Celebrate fall in Berkeley County, West Virginia with pumpkins, hayrides, and fun for the whole family</p>
          </div>
        </div>
        <div className={styles.bottom}>
          <div className={styles.cta}>
            <p>Available through August 20</p>
            <div className={styles.buttons}>
              <a href="/reservations" onClick={() =>
                track(
                  'Reservations',
                  { location: 'Hero Summer' }
                )
              }>
                <ArrowLeft size={24} weight="bold" />
                Book Your Event
              </a>
              <a href="https://docs.google.com/forms/d/e/1FAIpQLSdNLOwNjhKnsI4QT18MCGOrEvxXP164zfLpXQOZSSBcJQxo3A/viewform?usp=header" target="_blank" onClick={() => {
                track(
                  'Vendor Application',
                  { location: 'Hero Summer' }
                )
              }}>
                Vendor Application
                <ArrowSquareOut size={24} weight="bold" />
              </a>
            </div>
          </div>
          <div className={styles.seasonInfo}>
            <div className={styles.card}>
              <h2>Open Soon</h2>
              <p>We'e still getting ready for the season</p>
            </div>
          </div>
        </div>
      </div>
    </section>
  )
}
Enter fullscreen mode Exit fullscreen mode

This code is one of three seasonal hero components (Fall, Winter, and Summer) that share the same structure but differ in content. The problem is that the variable content (images, text, buttons) is tightly mixed in with the constant structure (layout, styling, markup). If I want to adjust the structure, I have to repeat that change in all three files.

Here’s where the concepts from our earlier Variable vs Constant table come into play: the layout, styling, and placement of elements are constants that should live in a single place, while text, images, and button data are variables that each season can define independently.


After:

Abstract Hero Component

export default function AbstractHero({
  backdrop,
  tagline = "Real Farm Fun",
  description = "Celebrate fall in Berkeley County, West Virginia with pumpkins, hayrides, and fun for the whole family",
  cta,
  seasonInfo
}) {
  return (
    <section className={styles.hero}>
      {backdrop && backdrop.src && typeof backdrop?.src === 'string' &&
        <Image
          src={backdrop.src}
          placeholder={backdrop.blurDataURL ? "blur" : undefined}
          blurDataURL={backdrop.blurDataURL || undefined}
          priority
          alt={backdrop.alt || "Seasonal Hero Backdrop"}
          width={1500}
          height={1500}
        ></Image>
      }
      <div className={styles.cover}>
        <div className={styles.top}>
          <h1 className={styles.tagline}>{tagline}</h1>
          <div>
            <p className={styles.description}>{description}</p>
          </div>
        </div>
        <div className={styles.bottom}>
          {cta &&
            <div className={styles.cta}>
              {cta.description &&
                <p>{cta.description}</p>
              }
              {cta.buttons && Array.isArray(cta.buttons) &&
                <div className={styles.buttons}>
                  {cta.buttons.map((button, index) => {
                    const isExternal = typeof button.href === 'string' && button.href.startsWith('http');
                    return (
                      <Link
                        key={index}
                        href={button.href}
                        onClick={button.onClick}
                        target={
                          isExternal ? '_blank' : undefined
                        }
                      >
                        {button.Icon &&
                          <button.Icon size={24} weight="bold" />
                        }
                        {button.label}
                      </Link>
                    )
                  })}
                </div>
              }
            </div>
          }
          {seasonInfo &&
            <div className={styles.seasonInfo}>
              <div className={styles.card}>
                {seasonInfo.title && <h2>{seasonInfo.title}</h2>}
                {seasonInfo.content && <p>{seasonInfo.content}</p>}
              </div>
            </div>
          }
        </div>
      </div>
    </section>
  )
}
Enter fullscreen mode Exit fullscreen mode

This is the base abstraction. All the constants from the earlier table live here: the layout, structure, styling, and placement. The variables are exposed as props: backdrop, tagline, description, cta, and seasonInfo. This means the seasonal hero components only need to supply the data that changes.

export default function HeroSummer() {
  return (
    <AbstractHero
      backdrop={{
        src: "/summer.webp",
        blurDataURL: "/summer-xs.webp"
      }}
      cta={{
        description: "Available through August 20",
        buttons: [
          {
            href: '/reservations',
            label: 'Book your event',
            onClick: () => {
              track(
                'Reservations',
                { location: 'Hero Summer' }
              )
            },
            Icon: Calendar
          },
          {
            href: 'https://docs.google.com/forms/d/e/1FAIpQLSdNLOwNjhKnsI4QT18MCGOrEvxXP164zfLpXQOZSSBcJQxo3A/viewform?usp=header',
            label: 'Apply to be a vendor',
            onClick: () => {
              track(
                'Vendor Application',
                { location: 'Hero Summer' }
              )
            },
            Icon: ArrowSquareOut
          }
        ]
      }}
      seasonInfo={{
        title: "Open Soon",
        content: "We'e still getting ready for the season"
      }}
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

Now the seasonal component is dramatically smaller and easier to read, containing only the variable data. Any change to the structure, layout, or styling happens in AbstractHero. This is the practical payoff of separating constants from variables in your abstraction.

Top comments (0)