Frontend Development 9 min read

Why Vue Component Design Becomes a Nightmare and How to Fix It

After three years of building Vue apps, the author reveals how naive component extraction leads to sprawling directories, tangled props and events, and mounting technical debt, then offers concrete strategies—clear responsibilities, minimal APIs, slots, and abstraction skills—to design maintainable, reusable components.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Why Vue Component Design Becomes a Nightmare and How to Fix It
Component design is the heavy trouble spot in Vue projects.

1. Extracting components ≠ reorganizing folders

Many beginners think componentization just means extracting UI that repeats.

Resulting components often look like:

<code><!-- TextInput.vue -->
<template>
<input :value="value" @input="$emit('update:value', $event.target.value)" />
</template>
</code>

When you need an input with an icon, you copy it:

<code><!-- IconTextInput.vue -->
<template>
<div class="icon-text-input">
<i class="icon" :class="icon" />
<input :value="value" @input="$emit('update:value', $event.target.value)" />
</div>
</template>
</code>

Later you add validation, loading, tooltip, and the component list explodes:

TextInput.vue
IconTextInput.vue
ValidatableInput.vue
LoadingInput.vue
FormInput.vue

Each component is a quick fix, not reusable.

2. Uncontrolled abstraction: reuse for reuse, nobody dares to use

Consider a “super complex” table component:

<code><CustomTable :columns="columns" :data="tableData" :show-expand="true" :enable-pagination="true" :custom-actions="['edit', 'delete']" />
</code>

It’s marketed as a generic component, but in practice:

Some pages need only display, no actions, but the component forces actions.

Custom sorting logic is impossible because it’s hard‑coded.

Styling conflicts with other UI libraries.

Warnings flood the console, making debugging hard.

Consequently, developers copy the component and modify it locally.

3. Data flows down, events flow up: Do you really understand props and emit?

Vue’s one‑way data flow states that parent components pass data via props, and children notify parents via emit.

Parent → props → child, child → emit → parent.

In reality:

Props are passed through seven layers, making data origins obscure.

Children emit multiple events, and parents pass callbacks back.

Developers resort to

inject/provide

,

ref

, or an

eventBus

to bypass the flow.

Example of deep nesting causing confusion:

<code><!-- Grandparent component -->
<template>
<PageWrapper>
<ChildComponent :formData="form" @submit="handleSubmit" />
</PageWrapper>
</template>

<!-- Child component -->
<template>
<Form :model="formData" />
<button @click="$emit('submit', formData)">Submit</button>
</template>
</code>

When the child is wrapped further, you lose track of which component controls the data and events.

4. Technical debt explosion: fear of deleting or changing

Component directories look tidy, but most components share these traits:

Many props and events with unclear usage.

Comments claim usage on page A, but pages B, C, D also import it.

A tiny change can cause a butterfly effect across the system.

Developers end up copying components and adding a

V2

suffix instead of refactoring.

<code>components/
├── Input.vue
├── InputV2.vue
├── InputWithTooltip.vue
├── InputWithValidation.vue
├── InputWithValidationV2.vue
└── ...
</code>

“To let others maintain my code, I decide not to touch it.”

5. The core of component design is “abstraction ability”

Vue component design’s difficulty lies not in syntax or encapsulation, but in your ability to abstract problems.

Example: a search area component

Naïve implementation binds many props:

<code><SearchHeader :keyword="keyword" :startDate="start" :endDate="end" @search="search" />
</code>

A better design provides slots and scoped slots:

<code><!-- SearchHeader.vue -->
<template>
<div class="search-header">
<slot name="form" />
<button @click="$emit('search')">Search</button>
</div>
</template>

<!-- Usage -->
<SearchHeader @search="search">
<template #form>
<el-input v-model="keyword" placeholder="Enter keyword" />
<el-date-picker v-model="range" type="daterange" />
</template>
</SearchHeader>
</code>

“Give structure to components, give behavior to pages. Components collaborate instead of controlling everything.”

6. How to design components correctly?

I summarize three simple yet effective recommendations:

✅ 1. Clarify component responsibilities: UI, interaction, or logic?

UI components only handle presentation (buttons, tags, cards).

Interaction components encapsulate user actions (inputs, selectors).

Logic components encapsulate business rules (filters, paginators).

Avoid mixing UI, logic, and API calls in a single component.

✅ 2. Keep props and emits minimal, expose only what’s required

If a component has more than six props, reconsider its design.

Abstract generic events like

click

into meaningful business events.

Do not use

ref

to manipulate child internals; it’s an anti‑pattern.

✅ 3. Use slots instead of overly customized props

When a component’s props become overly complex, replace them with slots:

<code><!-- Before -->
<SuperButton :label="'Submit'" :icon="'plus'" :iconPosition="'left'" :styleType="'primary'" :loading="true" />

<!-- After -->
<SuperButton>
<template #icon><PlusIcon /></template>
Submit
</SuperButton>
</code>

“Assign structure to components, assign behavior to pages. Components should collaborate, not control everything.”

Frontendbest practicesVueTechnical DebtComponent Design
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.