· Pantelis Theodosiou · Vue.js · 6 min read ·
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.
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

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.