An extremely common question I get asked all the time is, “how do you know when to split up a component?”
I want to share a simple pattern with you that is basically fool-proof, and can be applied to lots of components with almost no thought.
When we encounter a v-if
(or v-show
) in our template, there's one main pattern we can use:
Extracting the body of each branch into its own component.
This is just one of many patterns included in Clean Components. There, we go into much more detail, examining each pattern more closely and really fine-tuning our understanding.
Let's see how this one works in action.
When we extract each branch body we go from this:
<div v-if="condition"> <div> <!-- Lots of code here --> </div> </div> <div v-else> <div> <!-- Lots of other code --> </div> </div>
To this:
<div v-if="condition"> <NewComponent /> </div> <div v-else> <OtherComponent /> </div>
We know we can do this for two reasons:
We know that each branch is semantically related, meaning all of that code works together to perform the same task.
Each branch also does distinct work — otherwise, why have a v-if
at all?
This means it's a perfect opportunity to create new components.
And by replacing a large chunk of code with a well-named component that represents the code’s intent, we make our code much more self-documenting. But we’ll get to that later on.
Now let’s see how this works out in more detail.
We’ll be taking an Articles
component and seeing how we can refactor it using the Extract Conditional Pattern.
This is a component pulled straight from my blog, so you can see it in it’s two different states right on this website. We have a more detailed expanded view, and a more compact collapsed view (scroll to the very bottom of the article).
Here’s the code that we’ll be working with. Please take a moment to familiarize yourself with it:
<template> <div v-if="collapse" class="flex flex-col space-y-6 md:space-y-8"> <div v-for="article in filtered" :key="article.slug"> <NuxtLink class="article-link" :to="article.slug"> <h3 class="header-5 text-mt-light-blue"> {{ article.title }} </h3> </NuxtLink> </div> </div> <div v-else class="grid gap-y-16 gap-x-24 xl:gap-x-32 grid-cols-1 lg:grid-cols-2 mt-8" > <div v-for="(article, index) in filtered" v-show="index < currentLimit" :key="article.slug" class="space-y-5" > <NuxtLink class="article-link" :to="article.slug"> <h3 class="header-4 text-mt-light-blue"> {{ article.title }} </h3> </NuxtLink> <p class="subheader-4"> {{ article.formattedDate }} </p> <p>{{ article.description }}</p> </div> </div> </template>
This component uses a v-if
on the root element, so we can apply the Extract Conditional Pattern on it. This pattern also works with v-show
, but if you have neither in your component, you’re out of luck — you’ll have to use a different pattern.
The two branches of this v-if
do different things, even though they are fairly similar in function. One renders a collapsed view, the other an expanded view:
<template> <div v-if="collapse" class="flex flex-col space-y-6 md:space-y-8"> <!-- 👇 Collapsed view --> <div v-for="article in filtered" :key="article.slug"> <NuxtLink class="article-link" :to="article.slug"> <h3 class="header-5 text-mt-light-blue"> {{ article.title }} </h3> </NuxtLink> </div> </div> <div v-else class="grid gap-y-16 gap-x-24 xl:gap-x-32 grid-cols-1 lg:grid-cols-2 mt-8" > <!-- 👇 Expanded view --> <div v-for="(article, index) in filtered" v-show="index < currentLimit" :key="article.slug" class="space-y-5" > <NuxtLink class="article-link" :to="article.slug"> <h3 class="header-4 text-mt-light-blue"> {{ article.title }} </h3> </NuxtLink> <p class="subheader-4"> {{ article.formattedDate }} </p> <p>{{ article.description }}</p> </div> </div> </template>
Replacing each branch with a well-named component helps us understand what's going on here:
<template> <div v-if="collapse" class="flex flex-col space-y-6 md:space-y-8"> <ArticleCollapsed v-for="article in filtered" :key="article.slug" :article="article" /> </div> <div v-else class="grid gap-y-16 gap-x-24 xl:gap-x-32 grid-cols-1 lg:grid-cols-2 mt-8" > <ArticleExpanded v-for="(article, index) in filtered" v-show="index < currentLimit" :key="article.slug" :article="article" /> </div> </template>
Once we refactor, the difference in functionality between these two branches is much clearer! We don’t really need to think about the code, because the component names tell us exactly what’s happening here.
The first branch has ArticleCollapsed
— so it renders a collapsed view.
The second branch uses the ArticleExpanded
component — so we know that this is the expanded view.
Simplifying our code like this makes a lot of things a lot more obvious.
For example, because there is less clutter in this component, we can now more easily see that one branch uses flexbox for styling, while the other uses CSS grid. Maybe there’s a way to refactor that so the second branch also uses flexbox, simplifying our component further?
One thing to note: you don’t have to make your components as small as I did in this article.
I like smaller components, but sometimes too many small components becomes it’s own problem. I’m also trying to teach you this pattern through this article, which is much easier to do with shorter code examples.
To recap this pattern — for almost any v-if
that you encounter in a Vue template, you can break out the branches into their own components. We know this works.
(The only reason I don’t say all is that I’m sure there is an exception, I just haven’t found one yet.)
This is an extremely easy way to simplify our components and make them more readable, and doesn’t require a lot of thinking about abstractions or anything like that.
If you’re interested in learning more patterns like this, check out Clean Components. It’s full of valuable patterns like the Hidden Component Pattern and more.