· Pantelis Theodosiou · Vue.js  · 6 min read ·

1 views

Vue Design Patterns That Actually Make Your Life Easier

Learn practical Vue design patterns to build cleaner, scalable applications. Discover smart/dumb components, composables, provide/inject, and more patterns that help you write maintainable Vue code.

Learn practical Vue design patterns to build cleaner, scalable applications. Discover smart/dumb components, composables, provide/inject, and more patterns that help you write maintainable Vue code.

Most developers reach a point in a Vue project where things stop feeling simple. Components start multiplying, state gets scattered, and you find yourself scrolling through files wondering which version of yourself wrote this. This is usually the moment you start wishing for some structure. That is exactly where design patterns come in.

This article walks you through several practical Vue design patterns that help you build cleaner, scalable, and easier to maintain applications. Not theory for theory’s sake. Real, friendly, everyday patterns you can apply the moment you finish reading. You will also see diagrams and code examples along the way to keep things clear.

Why Vue developers need design patterns

Design patterns are proven solutions to recurring problems in software structure. Vue gives you great tools, but if you do not organize your app intentionally, even the Composition API cannot save you from chaos.

Using patterns helps you:

  • Keep your code predictable and logical
  • Reduce duplication and hidden side effects
  • Make refactoring easier
  • Help teammates (and your future self) understand your app faster

Let us go through the patterns that matter most.

Core patterns that keep your components clean

Smart and dumb components

One of the simplest and strongest patterns. Smart components handle data fetching, state, and decisions. Dumb components (also called presentational components) focus only on displaying UI based on props.

Smart Component

<!-- SmartParent.vue -->
<script setup>
import { ref, onMounted } from 'vue'
import UserList from './UserList.vue'

const users = ref([])
const loading = ref(true)

async function fetchUsers() {
  users.value = await api.getUsers()
  loading.value = false
}

onMounted(fetchUsers)
</script>

<template>
  <UserList :users="users" :loading="loading" />
</template>

Dumb Component

<!-- UserList.vue -->
<script setup>
defineProps({
  users: Array,
  loading: Boolean
})
</script>

<template>
  <div v-if="loading">Loading...</div>
  <ul v-else>
    <li v-for="user in users" :key="user.id">
      {{ user.name }}
    </li>
  </ul>
</template>

This separation keeps UI simple and logic easy to manage.

List and ListItem pattern

This pattern is perfect for any UI that displays collections.

The List component handles filtering, empty states, search fields, sorting and other logic. The ListItem component decides how a single item looks.

<!-- UsersList.vue -->
<script setup>
const props = defineProps({ users: Array })
const filter = ref('')

const filteredUsers = computed(() =>
  props.users.filter(u => u.name.includes(filter.value))
)
</script>

<template>
  <input v-model="filter" placeholder="Find user">
  <ul v-if="filteredUsers.length">
    <UserListItem
      v-for="u in filteredUsers"
      :key="u.id"
      :user="u"
    />
  </ul>
  <div v-else>No results</div>
</template>
<!-- UserListItem.vue -->
<script setup>
defineProps({ user: Object })
</script>

<template>
  <li>{{ user.name }} ({{ user.email }})</li>
</template>

This pattern makes it painless to evolve lists as your project grows.

Slot based components

If you find yourself passing five different props just to customize button text, card titles, or layout, you probably need slots.

<!-- AppButton.vue -->
<template>
  <button class="btn">
    <slot></slot>
  </button>
</template>

Usage:

<AppButton>
  Save Changes
</AppButton>

Or even:

<AppButton>
  <Icon name="check" /> Confirm
</AppButton>

Slots make components flexible, reusable, and future proof.

Composition API patterns

Composables for logic extraction

Composables work like tiny engines of logic. They hold state, side effects, computed values, watchers, and anything else that does not belong in a UI component.

// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(e) {
    x.value = e.pageX
    y.value = e.pageY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }
}

Use it anywhere:

<script setup>
import { useMouse } from './useMouse.js'
const { x, y } = useMouse()
</script>

<template>
  <div>Mouse at {{ x }}, {{ y }}</div>
</template>

This pattern improves readability and reduces duplication dramatically.

Provide and inject pattern

This pattern is ideal when you need to share state or behavior deeply in the component tree without passing props through every level.

Composable

// useTheme.js
import { ref, provide, inject } from 'vue'

const themeSymbol = Symbol('theme')

export function provideTheme() {
  const theme = ref('light')
  const toggle = () => {
    theme.value = theme.value === 'light' ? 'dark' : 'light'
  }
  provide(themeSymbol, { theme, toggle })
}

export function useTheme() {
  const injected = inject(themeSymbol)
  if (!injected) throw new Error('Theme not provided')
  return injected
}

Providing at the root

<script setup>
import { provideTheme } from './useTheme'
provideTheme()
</script>

<template>
  <RouterView />
</template>

Consuming anywhere

<script setup>
import { useTheme } from './useTheme'
const { theme, toggle } = useTheme()
</script>

<template>
  <button @click="toggle">Switch Theme ({{ theme }})</button>
</template>

Great for global concerns like user, permissions, settings, theme, and language.

Structural patterns for bigger apps

Strategy pattern

Use this when you have multiple possible behaviors for the same operation.

// strategies.js
export const strategies = {
  uppercase: (t) => t.toUpperCase(),
  lowercase: (t) => t.toLowerCase(),
  reverse: (t) => t.split('').reverse().join('')
}

export function applyStrategy(text, mode) {
  const fn = strategies[mode]
  return fn ? fn(text) : text
}

This keeps decision making clean and scalable.

Observer pattern through an event bus

Great for feature-level communication without coupling components.

// eventBus.js
import { reactive } from 'vue'

const store = reactive({})

export function useEventBus() {
  function on(event, callback) {
    if (!store[event]) store[event] = []
    store[event].push(callback)
  }

  function emit(event, payload) {
    if (!store[event]) return
    store[event].forEach(cb => cb(payload))
  }

  return { on, emit }
}

Now any component can listen or emit events.

Extra patterns developers find useful

Here are a few more that show up often in real projects.

Conditional rendering components

Instead of one huge component with nested v-if blocks, split each branch into smaller components.

Dynamic components

Render different components depending on the current state or tab.

<component :is="currentView"></component>

Recursive components

Ideal for rendering trees like menus, folders, comments, dependencies.

Form builder pattern

A generic form renderer that takes a JSON schema and generates fields, validation, and layout.

Diagram: How these layers fit together

Typical flow in a structured Vue application

This is a simplified view but shows the typical flow in a structured Vue application.

When patterns help and when they hurt

Patterns shine when:

  • Your app has many components
  • You see repeated logic
  • Your state becomes complicated
  • You want cleaner separation of concerns

Patterns can hurt when:

  • Your app is tiny
  • You over abstract too early
  • You solve problems you do not actually have
  • Use patterns as a toolkit. Not as a checklist.

Final thoughts

Vue is flexible, which is great, but it also means your app structure is mostly your own responsibility. Design patterns help you keep things healthy and understandable as the codebase grows. Start with smart vs dumb components and composables, then expand to provide-inject, list patterns, dynamic components, and more advanced architectural patterns when needed.

Back to Blog

Related Posts

View All Posts »
Dynamic Menu in Vue

Dynamic Menu in Vue

Learn how to create a dynamic navigation menu in Vue.js using Vue Router meta fields and recursive components. Build a flexible menu system that automatically generates navigation from your route configuration.

State Management Showdown: Vuex vs Pinia

State Management Showdown: Vuex vs Pinia

A comprehensive comparison of Vuex and Pinia for Vue.js state management. Learn the key differences, code examples, and which library is best suited for your project based on API design, TypeScript support, and performance.

Vue 3 State to Your CSS with v-bind()

Vue 3 State to Your CSS with v-bind()

Learn how to bind Vue 3 component state directly to CSS properties using the v-bind() CSS function. Create dynamic styles with reactive state management in Vue 3 single-file components.

Google Analytics on Gridsome Applications

Google Analytics on Gridsome Applications

Learn how to integrate Google Analytics into your Gridsome static site manually for better customization. Step-by-step guide to tracking pageviews and custom events using gtag.js.