About us Guides Projects Contacts
Админка
please wait

Vue.js excels at building reusable, maintainable components. Understanding proven component patterns—from simple props to complex composition—separates adequate Vue code from excellent Vue applications. This guide covers essential Vue component patterns from a senior developer's perspective.

Why Component Patterns Matter

Well-designed components enable:

  1. Reusability: Write once, use everywhere
  2. Testability: Isolated, predictable behavior
  3. Maintainability: Clear responsibilities and boundaries
  4. Scalability: Patterns that grow with your app
  5. Team Collaboration: Consistent, understandable code

Props and Events

Props Down, Events Up

<!-- Parent.vue -->
<template>
<TodoItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
@toggle="handleToggle"
@delete="handleDelete"
/>
</template>
<script setup>
import { ref } from 'vue';
import TodoItem from './TodoItem.vue';
const todos = ref([
{ id: 1, text: 'Learn Vue', done: false },
{ id: 2, text: 'Build app', done: false }
]);
function handleToggle(id) {
const todo = todos.value.find(t => t.id === id);
if (todo) todo.done = !todo.done;
}
function handleDelete(id) {
todos.value = todos.value.filter(t => t.id !== id);
}
</script>
<!-- TodoItem.vue -->
<template>
<div class="todo-item">
<input
type="checkbox"
:checked="todo.done"
@change="$emit('toggle', todo.id)"
/>
<span :class="{ done: todo.done }">{{ todo.text }}</span>
<button @click="$emit('delete', todo.id)">Delete</button>
</div>
</template>
<script setup>
defineProps({
todo: {
type: Object,
required: true,
validator: (todo) => {
return todo.id !== undefined &&
typeof todo.text === 'string' &&
typeof todo.done === 'boolean';
}
}
});
defineEmits(['toggle', 'delete']);
</script>
<style scoped>
.done {
text-decoration: line-through;
color: gray;
}
</style>

v-model on Custom Components

<!-- CustomInput.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
v-bind="$attrs"
/>
</template>
<script setup>
defineProps(['modelValue']);
defineEmits(['update:modelValue']);
</script>
<!-- Usage -->
<CustomInput v-model="searchQuery" placeholder="Search..." />

Multiple v-model Bindings

<!-- UserForm.vue -->
<template>
<div>
<input
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
placeholder="First Name"
/>
<input
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
placeholder="Last Name"
/>
</div>
</template>
<script setup>
defineProps(['firstName', 'lastName']);
defineEmits(['update:firstName', 'update:lastName']);
</script>
<!-- Usage -->
<UserForm v-model:firstName="user.firstName" v-model:lastName="user.lastName" />

Slots for Composition

Default and Named Slots

<!-- Card.vue -->
<template>
<div class="card">
<div class="card-header" v-if="$slots.header">
<slot name="header" />
</div>
<div class="card-body">
<slot /> <!-- Default slot -->
</div>
<div class="card-footer" v-if="$slots.footer">
<slot name="footer" />
</div>
</div>
</template>
<!-- Usage -->
<Card>
<template #header>
<h2>Card Title</h2>
</template>
<p>This is the main content of the card.</p>
<template #footer>
<button>Action</button>
</template>
</Card>

Scoped Slots (Render Props Pattern)

<!-- DataList.vue -->
<template>
<div>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<div v-else>
<slot :items="items" :refresh="fetchData" />
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const props = defineProps({
url: { type: String, required: true }
});
const items = ref([]);
const loading = ref(true);
const error = ref(null);
async function fetchData() {
loading.value = true;
error.value = null;
try {
const response = await fetch(props.url);
items.value = await response.json();
} catch (e) {
error.value = e.message;
} finally {
loading.value = false;
}
}
onMounted(fetchData);
</script>
<!-- Usage -->
<DataList url="/api/users" v-slot="{ items, refresh }">
<ul>
<li v-for="user in items" :key="user.id">
{{ user.name }}
</li>
</ul>
<button @click="refresh">Refresh</button>
</DataList>

Composables (Composition API)

Extract Reusable Logic

// composables/useFetch.js
import { ref, watchEffect } from 'vue';
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
const loading = ref(true);
watchEffect(async () => {
loading.value = true;
error.value = null;
try {
const response = await fetch(url.value || url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
data.value = await response.json();
} catch (e) {
error.value = e.message;
} finally {
loading.value = false;
}
});
return { data, error, loading };
}
// composables/useLocalStorage.js
import { ref, watch } from 'vue';
export function useLocalStorage(key, defaultValue) {
const stored = localStorage.getItem(key);
const value = ref(stored ? JSON.parse(stored) : defaultValue);
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue));
}, { deep: true });
return value;
}
// composables/useDebounce.js
import { ref, watch } from 'vue';
export function useDebounce(source, delay = 300) {
const debounced = ref(source.value);
let timeout;
watch(source, (newValue) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
debounced.value = newValue;
}, delay);
});
return debounced;
}

Using Composables

<template>
<div>
<input v-model="searchTerm" placeholder="Search users..." />
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<ul v-else>
<li v-for="user in data" :key="user.id">{{ user.name }}</li>
</ul>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useFetch } from '@/composables/useFetch';
import { useDebounce } from '@/composables/useDebounce';
const searchTerm = ref('');
const debouncedSearch = useDebounce(searchTerm, 500);
const url = computed(() =>
debouncedSearch.value
? `/api/users?search=${debouncedSearch.value}`
: '/api/users'
);
const { data, error, loading } = useFetch(url);
</script>

Provide/Inject for Deep Props

<!-- ThemeProvider.vue -->
<template>
<slot />
</template>
<script setup>
import { provide, ref, readonly } from 'vue';
const theme = ref('light');
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light';
}
// Provide readonly to prevent child mutation
provide('theme', readonly(theme));
provide('toggleTheme', toggleTheme);
</script>
<!-- ThemedButton.vue (deeply nested) -->
<template>
<button
@click="toggleTheme"
:class="['btn', `btn-${theme}`]"
>
<slot />
</button>
</template>
<script setup>
import { inject } from 'vue';
const theme = inject('theme', 'light');
const toggleTheme = inject('toggleTheme', () => {});
</script>

Renderless Components

<!-- MouseTracker.vue -->
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const x = ref(0);
const y = ref(0);
function update(event) {
x.value = event.pageX;
y.value = event.pageY;
}
onMounted(() => window.addEventListener('mousemove', update));
onUnmounted(() => window.removeEventListener('mousemove', update));
// Expose to slot
defineExpose({ x, y });
</script>
<template>
<slot :x="x" :y="y" />
</template>
<!-- Usage -->
<MouseTracker v-slot="{ x, y }">
<div>Mouse: {{ x }}, {{ y }}</div>
</MouseTracker>

Component Communication

Event Bus (Small Apps)

// eventBus.js
import mitt from 'mitt';
export const bus = mitt();
// ComponentA.vue
import { bus } from '@/eventBus';
bus.emit('user-selected', { id: 1 });
// ComponentB.vue
import { bus } from '@/eventBus';
import { onMounted, onUnmounted } from 'vue';
onMounted(() => {
bus.on('user-selected', handleUserSelected);
});
onUnmounted(() => {
bus.off('user-selected', handleUserSelected);
});

Pinia Store (Larger Apps)

// stores/user.js
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
currentUser: null,
users: []
}),
getters: {
isLoggedIn: (state) => !!state.currentUser,
userById: (state) => (id) => state.users.find(u => u.id === id)
},
actions: {
async fetchUsers() {
const response = await fetch('/api/users');
this.users = await response.json();
},
async login(credentials) {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
});
this.currentUser = await response.json();
},
logout() {
this.currentUser = null;
}
}
});
<!-- Using Pinia -->
<script setup>
import { useUserStore } from '@/stores/user';
import { storeToRefs } from 'pinia';
const userStore = useUserStore();
const { currentUser, isLoggedIn } = storeToRefs(userStore);
// Actions can be called directly
userStore.fetchUsers();
</script>

Dynamic Components

<template>
<component
:is="currentComponent"
v-bind="componentProps"
@action="handleAction"
/>
<button @click="switchComponent('FormA')">Form A</button>
<button @click="switchComponent('FormB')">Form B</button>
</template>
<script setup>
import { ref, computed, shallowRef, markRaw } from 'vue';
import FormA from './FormA.vue';
import FormB from './FormB.vue';
const components = {
FormA: markRaw(FormA),
FormB: markRaw(FormB)
};
const currentName = ref('FormA');
const currentComponent = computed(() => components[currentName.value]);
const componentProps = computed(() => ({
title: `This is ${currentName.value}`
}));
function switchComponent(name) {
currentName.value = name;
}
</script>

Async Components

import { defineAsyncComponent } from 'vue';
// Basic async component
const AsyncModal = defineAsyncComponent(() =>
import('./components/Modal.vue')
);
// With loading and error states
const AsyncChart = defineAsyncComponent({
loader: () => import('./components/Chart.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200, // Show loading after 200 ms
timeout: 10000 // Timeout after 10 s
});

Key Takeaways

  1. Props down, events up: Maintain unidirectional data flow
  2. Composables for logic reuse: Extract and share reactive logic
  3. Slots for flexibility: Let parents control rendering
  4. Provide/Inject for deep props: Avoid prop drilling
  5. Pinia for global state: When component props aren't enough
  6. Async components for performance: Code-split large components

Vue's component system is remarkably flexible—choose patterns that match your application's complexity rather than overengineering from the start.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive