The Hidden Components Pattern

There might be components hidden inside of your existing components.

Finding them and extracting them will make your code simpler, and easier to use.

In this article, we’ll take a detailed look at the Hidden Component pattern. Here’s where we’re going:

  • Overview of Hidden Components
  • Refactoring a real-world example
  • Steps of the pattern
  • Nuances to keep in mind when using this pattern

By the end of this article, you’ll be able to identify any Hidden Components in your code, extract them, and make your components so much simpler!

Oh, I can’t forget to mention this — this article is adapted from material for my upcoming course, Clean Components. So if you like this, stay in touch for more details about the course.

The best way to do that is to sign up for my newsletter.

Overview of Hidden Components

Looking at a component itself is the primary way that we can figure out when and how to refactor it. But we can also look at how the component is used for some clues.

Specifically, we’re looking to see if there are subsets of this component where those features are only used together. This suggests that there may be more than one component hidden inside of this one.

Let's say we have the following component:

<template>
  <div v-if="conditional">
    <!-- ... -->
  </div>
  <div v-else>
    <!-- ... -->
  </div>
</template>

Because the v-if is at the root, we know that it's not actually adding any value to this component.

Instead, we can simplify by splitting into one component for each branch of our conditional:

<template>
  <ComponentWhereConditionalIsTrue />
  <ComponentWhereConditionalIsFalse />
</template>

Now, we don't need to hard-code the conditional prop — we can just use the more descriptive and specific component.

For another example, if prop1 and prop2 are only ever used together, but never with prop3 and prop4, it could mean that the functionality relying on prop1 and prop2 should be separated from the rest of the component.

In this illustration, the usage of MyComponent always uses two distinct sets of props, prop1 and prop2, or prop3 and prop4:

<MyComponent prop-1="someValue" prop-2="anotherValue" />

<MyComponent prop-1="hello" prop-2="world" />

<MyComponent :prop-3="34" prop-4 />

In our theoretical refactoring, we would split the component to work like this:

<FirstComponent prop-1="someValue" prop-2="anotherValue" />

<FirstComponent prop-1="hello" prop-2="world" />

<SecondComponent :prop-3="34" prop-4 />

Let’s look at a more interesting example, shall we?

Refactoring

Here we have our Articles component. It’s responsible for listing out articles, in either a collapsed or expanded form.

It’s actually the component that powers the article section at the end of each blog post, as well as on the /articles page:

<template>
  <div
    ref="container"
    class="w-full pb-12 md:pb-24 flex flex-col"
    :class="!collapse && 'pt-16 md:pt-20'"
  >
    <div class="flex flex-col justify-center">
      <div
        v-if="!collapse"
        class="border-b-2 border-gray-300 border-dotted pb-3"
      >
        <h2 ref="title" class="text-4xl font-bold mb-6">Articles</h2>

        <TagList
          :tags="tagList"
          :selected-tag-index="selectedTagIndex"
          @select-tag="index => selectTag(index)"
        />
      </div>

      <ArticleListWithLimit
        :articles="filtered"
        :current-limit="currentLimit"
        :collapse="collapse"
      />

      <div v-if="!collapse" class="flex justify-center">
        <button
          v-if="hasMoreArticles"
          class="article-btn focus-outline"
          @click="currentLimit += limit"
        >
          Load more
        </button>
        <EndOfArticles
          v-else
          :selected-tag="selectedTag"
          @filter-by-tag="scrollToTagList"
          @see-all-articles="selectTag(tagList.length - 1)"
        />
      </div>
    </div>
  </div>
</template>

If you examine this closely, you may notice that although there is some shared functionality between the expanded and collapsed versions, they are mostly “orthogonal” to each other.

Orthoganality is a mathematical term essentially meaning, “they do not overlap”.

Yes, it’s a bit jargony, but it’s the best word for what I’m trying to describe here and a useful concept in software engineering and mathematics.

But what I’m getting at is that we really have two distinct components here, based on collapse either being true or false: ArticlesCollapsed and ArticlesExpanded.

When collapsed is true we don't even render half of this component:

<template>
  <div
    ref="container"
    class="w-full pb-12 md:pb-24 flex flex-col"
    :class="!collapse && 'pt-16 md:pt-20'"
  >
    <div class="flex flex-col justify-center">

      <!-- 👇 This div is not rendered -->
      <div
        v-if="!collapse"
        class="border-b-2 border-gray-300 border-dotted pb-3"
      >
        <h2 ref="title" class="text-4xl font-bold mb-6">Articles</h2>

        <TagList
          :tags="tagList"
          :selected-tag-index="selectedTagIndex"
          @select-tag="index => selectTag(index)"
        />
      </div>
      <!-- 👆 This div is not rendered -->

      <ArticleListWithLimit
        :articles="filtered"
        :current-limit="currentLimit"
        :collapse="collapse"
      />

       <!-- 👇 This div is not rendered -->
      <div v-if="!collapse" class="flex justify-center">
        <button
          v-if="hasMoreArticles"
          class="article-btn focus-outline"
          @click="currentLimit += limit"
        >
          Load more
        </button>
        <EndOfArticles
          v-else
          :selected-tag="selectedTag"
          @filter-by-tag="scrollToTagList"
          @see-all-articles="selectTag(tagList.length - 1)"
        />
      </div>
      <!-- 👆 This div is not rendered -->

    </div>
  </div>
</template>

To simplify this process a bit, we’ll first do a bit of refactoring. It’s always helpful to turn a complicated problem into a simpler problem before solving it.

We'll extract a child component whenever we encounter the collapse prop in a conditional. Doing this, we create two new components: ArticlesHeader and ArticlesFooter.

Here is ArticlesHeader:

<template>
  <div
    class="border-b-2 border-gray-300 border-dotted pb-3"
  >
    <h2 ref="title" class="text-4xl font-bold mb-6">Articles</h2>
    <TagList
      :tags="tagList"
      :selected-tag-index="selectedTagIndex"
      @select-tag="$emit('select-tag', index)"
    />
  </div>
</template>

And here is ArticlesFooter:

<div class="flex justify-center">
  <button
    v-if="hasMoreArticles"
    class="article-btn focus-outline"
    @click="$emit('load-more')"
  >
    Load more
  </button>
  <EndOfArticles
    v-else
    :selected-tag="selectedTag"
    @filter-by-tag="$emit('scroll-to-taglist')"
    @see-all-articles="$emit('see-all-articles')"
  />
</div>

Now we can rewrite the component based on the collapse prop and our new components:

<template>
  <div
    ref="container"
    class="w-full pb-12 md:pb-24 flex flex-col"
    :class="!collapse && 'pt-16 md:pt-20'"
  >
    <div class="flex flex-col justify-center">
      <ArticlesHeader v-if="!collapsed" ... />
      <ArticleListWithLimit
        :articles="filtered"
        :current-limit="currentLimit"
        :collapse="collapse"
      />
      <ArticlesFooter v-if="!collapse" ... />
    </div>
  </div>
</template>

Okay, so now we’ve simplified the component, and it’s easier for us to understand.

Here, we can see that the ArticlesHeader and ArticlesFooter components should only be in the ArticlesExpanded component, where collapse is always false.

<template>
  <div
    ref="container"
    class="w-full pb-12 md:pb-24 flex flex-col"
    :class="!collapse && 'pt-16 md:pt-20'"
  >
    <div class="flex flex-col justify-center">
      <!-- 👇 Only rendered when collapse === false -->
      <ArticlesHeader v-if="!collapsed" ... />
      <ArticleListWithLimit
        :articles="filtered"
        :current-limit="currentLimit"
        :collapse="collapse"
      />
      <!-- 👇 Only rendered when collapse === false -->
      <ArticlesFooter v-if="!collapse" ... />
    </div>
  </div>
</template>

The ArticleListWithLimit will need to be passed a collapse prop based on which component we're in:

<template>
  <div
    ref="container"
    class="w-full pb-12 md:pb-24 flex flex-col"
    :class="!collapse && 'pt-16 md:pt-20'"
  >
    <div class="flex flex-col justify-center">
      <ArticlesHeader v-if="!collapsed" ... />

      <!-- 👇 Always uses the value of collapse -->
      <ArticleListWithLimit
        :articles="filtered"
        :current-limit="currentLimit"
        :collapse="collapse"
      />
      <ArticlesFooter v-if="!collapse" ... />
    </div>
  </div>
</template>

We also need to take care of these conditional styles when refactoring.

<template>
  <div
    ref="container"
    class="w-full pb-12 md:pb-24 flex flex-col"
    :class="!collapse && 'pt-16 md:pt-20'" 👈 Conditional styles
  >
    <div class="flex flex-col justify-center">
      <ArticlesHeader v-if="!collapsed" ... />
      <ArticleListWithLimit
        :articles="filtered"
        :current-limit="currentLimit"
        :collapse="collapse"
      />
      <ArticlesFooter v-if="!collapse" ... />
    </div>
  </div>
</template>

Taking all of those details into account, we can create our two new components.

Here is our new ArticlesExpanded component:

<template>
  <div
    ref="container"
    class="w-full pb-12 md:pb-24 pt-16 md:pt-20 flex flex-col "
  >
    <div class="flex flex-col justify-center">
      <ArticlesHeader ... />
      <ArticleListWithLimit
        :articles="filtered"
        :current-limit="currentLimit"
      />
      <ArticlesFooter ... />
    </div>
  </div>
</template>

And here's the new ArticlesCollapsed component:

<template>
  <div
    ref="container"
    class="w-full pb-12 md:pb-24 flex flex-col"
  >
    <div class="flex flex-col justify-center">
      <ArticleListWithLimit
        :articles="filtered"
        :current-limit="currentLimit"
        collapse
      />
    </div>
  </div>
</template>

Refactoring Steps

To recap, here are the basic steps for performing this refactor:

  1. Look for how the component is being used
  2. Identify any subsets of behaviour — props, events, slots, etc. — that don't overlap
  3. Simplify using other patterns until it's easy to understand
  4. Refactor into separate components based on the subsets of behaviour

Things to keep in mind

When using this pattern there are some nuances and details to consider:

  • When to avoid this pattern: Dynamic vs. Hard-coded values
  • Temporary component for refactoring

Let’s take a look at both of these.

When to avoid this pattern: Dynamic vs. Hard-Coded

It’s just as important to know when not to use a pattern as it is to know how to use a pattern. For Hidden Components, it comes down to whether we’re dealing with dynamic, reactive values, or values that are hard-coded.

We now have two new components, used like this:

<ArticlesCollapsed />

<ArticlesExpanded />

But if we always use these components where we're dynamically switching between them, we now have to use a wrapper component:

<!-- Articles.vue -->
<template>
  <ArticlesCollapsed v-if="collapse" />
  <ArticlesExpanded v-else />
</template>

Our code is easier to understand, but we haven't necessarily simplified our code. In a way, we’re back to where we started, switching between behaviours based on the collapse prop.

This is because if we always use collapse dynamically, then the collapsed and expanded versions aren't really separate components anymore. They're two distinct ways of using the same component.

So in this case, if we’re only using collapse dynamically, we don’t gain much by having separated these components. Yes, it is more organized, but not much simpler.

But we may mix dynamic and hard-coded usage of this component throughout our app.

This makes our wrapper component quite useful, since we now have three options to choose from now. Each one very clearly shows the intended usage, making our code easy to understand:

<ArticlesCollapsed />

<ArticlesExpanded />

<Articles :collapse="isCollapsed" />

To sum up:

  • Replace hard-coded usage with ArticlesCollapsed and ArticlesExpanded
  • Replace dynamic usage with the wrapper component

Simplifying the Refactoring Process

This wrapper component also makes it easier to refactor, especially if you're using this component all over your app.

When refactoring this component, instead of getting rid of Articles.vue and breaking every component that uses it, we can replace it with the wrapper.

Now, our app continues to work as expected.

We can replace each usage of the Articles component with ArticlesCollapsed or ArticlesExpanded as necessary, knowing that our app continues to work at each stage.

By using the wrapper component in this way, we can incrementally refactor our application, ensuring that it still works at each step along the way.

Conclusion

The Hidden Component pattern is a great way to find components stuck inside of other components, just waiting to be set free.

As we’ve seen, finding them and extracting them isn’t too complicated, but does require some work.

There are also a few nuances to keep in mind — whether it’s being used more dynamically, or being hard-coded, and making sure to refactor incrementally to avoid breaking your app.

And don’t forget — this article is adapted from material for my upcoming course, Clean Components. So if you like this, stay in touch for more details about the course!

The best way to do that is to sign up for my newsletter.

🎉 Get 30% off Vue Tips Collection!
Get it now!