Skip to content
⚠️ This article was written in 2018. Some content may be outdated.

Vue Component Design Principles

After two years writing Vue, I've started reflecting on how components should be designed. Here are several principles for making components more maintainable.

Single Responsibility

Each component does one thing:

javascript
// ❌ One component doing too much
// UserPage.vue: shows user info + handles permissions + manages pagination + calls APIs
export default {
  data() {
    return { user: null, permissions: [], list: [], page: 1 };
  },
  created() {
    this.fetchUser();
    this.fetchPermissions();
    this.fetchList();
  },
  // 200 lines of code...
};

// ✅ Separate concerns
// UserProfile.vue: only displays user info
// PermissionPanel.vue: permission management
// UserList.vue: list display (including pagination)
// UserPage.vue: composes the above, handles page-level logic

Keep Props Atomic

javascript
{% raw %}
// ❌ Passing the entire user object — component depends on its structure
props: { user: Object }
// In template: {{ user.profile.avatar }}

// ✅ Pass only what the component actually needs
props: {
  name: String,
  avatarUrl: String,
  role: String
}
{% endraw %}

Benefits: components are easier to test and their dependencies are clearer.

Event Naming: Use Verbs

javascript
// ❌ Vague event names
this.$emit("click");
this.$emit("change");
this.$emit("update");

// ✅ Event names describe what happened
this.$emit("user:saved", savedUser);
this.$emit("filter:changed", newFilter);
this.$emit("item:deleted", itemId);

Never Mutate Props Directly

javascript
// ❌ Mutating the prop (Vue will warn)
props: { value: String },
methods: {
  clear() { this.value = '' }  // Can't do this!
}

// ✅ Emit an event and let the parent update
props: { value: String },
methods: {
  clear() { this.$emit('update:value', '') }
}

// Parent component
<MyInput :value="text" @update:value="text = $event" />
// Or with .sync shorthand
<MyInput :value.sync="text" />

Configurable Defaults

javascript
props: {
  size: {
    type: String,
    default: 'medium',
    validator: (v) => ['small', 'medium', 'large'].includes(v)
  },
  loading: {
    type: Boolean,
    default: false
  }
}

Controlled vs Uncontrolled

Controlled component: data is controlled by the parent (via v-model / props)

javascript
// Controlled: parent controls the value
<SearchInput :value="searchText" @input="searchText = $event" />

// Uncontrolled: manages its own internal state
// Only emits results via events when needed
<DatePicker @change="handleDateSelect" />

Choose based on the use case — not all state needs to be lifted to the parent.

MIT Licensed