Организовать обмен [реактивными] данными между компонентами и модулями во Vue 3 приложении можно несколькими способами.
1. Prop drilling
Prop drilling - это ситуация, когда пропсы передаются через несколько компонентов, которые сами их не используют, только чтобы доставить данные до компонента, которому они действительно нужны.
Рассмотрим пример:
<script setup>
const user = {
name: 'John',
email: 'john@example.com'
}
</script>
<template>
<div>
<Header :user="user" />
</div>
</template>
<!-- Header.vue -->
<script setup>
const props = defineProps(['user'])
</script>
<template>
<header>
<Nav :user="user" />
</header>
</template>
<script setup>
const props = defineProps(['user'])
</script>
<template>
<nav>
<UserMenu :user="user" />
</nav>
</template>
<!-- UserMenu.vue -->
<script setup>
const props = defineProps(['user'])
</script>
<template>
<div>
{{ user.name }}
</div>
</template>
Как видно, объект user
передается через несколько компонентов, пока не достигает UserMenu
, где он действительно используется. Это и есть prop drilling.
Схема передачи данных при prop drilling:
App.vue (:user)
|
+-- Header.vue (:user)
|
+-- Nav.vue (:user)
|
+-- UserMenu.vue (использует user)
Плюсы:
Явная передача данных
Легко отследить поток данных
Не требует дополнительных инструментов
Хорошо работает для небольших приложений
Минусы:
Загромождает код при большом количестве пропсов
Усложняет рефакторинг
Компоненты-посредники получают ненужные им пропсы
Сложно поддерживать при глубокой вложенности
2. Provide/Inject
Vue предоставляет встроенный механизм provide / inject для передачи данных через несколько уровней компонентов:
<!-- App.vue -->
<script setup>
import { provide } from 'vue'
const user = {
name: 'John',
email: 'john@example.com'
}
provide('user', user)
</script>
<!-- UserMenu.vue -->
<script setup>
import { inject } from 'vue'
const user = inject('user')
</script>
<template>
<div>
{{ user.name }}
</div>
</template>
Схема работы provide/inject:
App.vue
|
+-- provide('user')
|
+-- Header.vue
|
+-- Nav.vue
|
+-- UserMenu.vue
|
+-- inject('user')
Плюсы:
Встроенное решение Vue
Простота использования
Не требует дополнительных зависимостей
Подходит для передачи данных через много уровней
Минусы:
Сложно отследить источник данных
Нет поддержки Vue DevTools
Возможны конфликты имен инъекций
Нет встроенной типизации (требуется дополнительная настройка TypeScript)
3. Composition API
Другой подход - использование Vue Reactivity API
напрямую, с экспортом реактивного стейта из js
модуля:
// useUser.js
import { ref } from 'vue'
export const user = ref({
name: 'John',
email: 'john@example.com'
})
export function updateUser(newUser) {
// ...
}
<!-- UserMenu.vue -->
<script setup>
import { user } from './useUser'
</script>
<template>
<div>
{{ user.name }}
</div>
</template>
Данный подход иногда называется глобальными рефами или модульными рефами.
Схема работы Composition API:
useUser.js (ref + методы)
|
+-- ComponentA.vue (импорт + использование)
|
+-- ComponentB.vue (импорт + использование)
|
+-- ComponentC.vue (импорт + использование)
Можно держать в одном файле и сам ref
, и логику его работы, получив аналог объекта ООП.
С данной конструкцией можно попасть на цикличные js
импорты - если А импортирует Б, в котором определен б = ref()
, и Б импортирует С, который использует б (например, в watch
), то в С получится "не определенный" б.
Явное создание синглтона будет более грамотным решением (например, создание composable функции и export/provide её на все приложение, либо другие вариации).
Плюсы:
Отличная инкапсуляция логики
Позволяет в полной мере использовать возможности
Vue Reactivity API
Легко тестировать
Хорошая переиспользуемость
Явные зависимости
Минусы:
Требует правильной структуризации кода
Нет централизованного управления состоянием
Сложнее отлаживать
Возможны цикличные зависимости
4. Глобальное хранилище (Pinia)
Для более сложных случаев можно использовать менеджер состояний Pinia
:
// stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
user: {
name: 'John',
email: 'john@example.com'
}
})
})
<!-- UserMenu.vue -->
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
</script>
<template>
<div>
{{ userStore.user.name }}
</div>
</template>
Схема работы Pinia:
stores/user.js (state + actions + getters)
|
+-- DevTools <--> Pinia Store
|
+-- ComponentA.vue (useUserStore())
|
+-- ComponentB.vue (useUserStore())
|
+-- ComponentC.vue (useUserStore())
Плюсы:
Централизованное управление состоянием
Интеграция с Vue DevTools
Поддержка
SSR
Встроенная поддержка TypeScript
Лучше подходит для работы в большой команде
Минусы:
Дополнительная зависимость
Падение производительности по сравнению с просто использованием ref
Избыточно для небольших приложений
Требует настройки
Больше кода для простых случаев
5. Event Bus
Еще один подход - использование событийной шины (EventBus
). Хотя во Vue 3 нет встроенного Event Bus как в Vue 2, мы можем использовать внешние библиотеки вроде mitt
:
// eventBus.js
import mitt from 'mitt'
export const eventBus = mitt()
// Типизация событий (опционально)
type Events = {
'user-updated': { name: string, email: string }
'user-deleted': { id: number }
}
export const typedEventBus = mitt<Events>()
Использование:
<!-- ComponentA.vue -->
<script setup>
import { eventBus } from './eventBus'
const sendUser = () => {
eventBus.emit('user-updated', {
name: 'John',
email: 'john@example.com'
})
}
</script>
<!-- ComponentB.vue -->
<script setup>
import { eventBus } from './eventBus'
import { onMounted, onUnmounted } from 'vue'
const handleUser = (user) => {
console.log('Получены данные пользователя:', user)
}
onMounted(() => {
eventBus.on('user-updated', handleUser)
})
onUnmounted(() => {
eventBus.off('user-updated', handleUser)
})
</script>
Использование EventBus
вместе в системой реактивности Vue
, чаще всего, неоправданно и вносит путаницу в код, поэтому его иногда называют антипаттерном для фронтенда.
Есть случаи, когда EventBus
может быть полезен, например, при реализации работы с WebSocket
. Но в этом случае он работает как Proxy
или Facade
для WS клиента (чтобы сделать возможным переподключение при разрыве соединения), а не как часть реактивной системы.
Схема работы Event Bus:
eventBus.js
|
+-- ComponentA.vue (emit)
| |
| +-- событие 'user-updated' ----+
| |
+-- ComponentB.vue (<---------------+)
| |
| +-- on('user-updated')
|
+-- ComponentC.vue
|
+-- on('user-updated')
Плюсы:
Простая реализация
Гибкость в коммуникации между компонентами
Не требует иерархической связи между компонентами
Легко добавлять новых подписчиков
Минусы:
Сложно отслеживать поток данных
Возможны утечки памяти при неправильной очистке
Нет типизации событий по умолчанию
Когда что использовать?
-
Prop Drilling - оптимален когда:
Приложение имеет простую и неглубокую структуру компонентов
Количество передаваемых пропсов невелико
Данные нужны только в ближайших компонентах
Нет необходимости в сложной логике управления состоянием
Важна максимальная прозрачность передачи данных
Проект небольшой или находится в стадии прототипирования
-
Provide/Inject - хорошо подходит для:
Передачи данных через много уровней компонентов
Когда данные нужны только в определенной ветке дерева компонентов
В случаях с темизацией или локализацией
-
Composition API - оптимально когда:
Нужна переиспользуемая логика
Данные используются в разных частях приложения
Требуется инкапсуляция сложной логики
-
Pinia - лучший выбор если:
Нужно глобальное состояние
Нужна поддержка
SSR
Необходима поддержка Vue DevTools
Важна типизация (TypeScript)
-
Event Bus - может быть полезен в случаях:
Когда основная система передачи данных построена на нем
Работы с WebSocket
Специфичных случаях
Простой коммуникации между несвязанными компонентами
Выводы
Prop drilling - не всегда проблема
Provide/inject - простое встроенное решение
Модульные рефы - мощный инструмент для переиспользуемой логики
Pinia - комплексное решение для управления состоянием
Event Bus - может быть полезен в специфичных случаях
Выбор подхода зависит от конкретного случая и требований проекта. Иногда простая передача пропсов может быть самым понятным решением.
Другая интересная и полезная информация о Vue.js
и фронтенде в целом на нашем Телеграм‑канале: @vuefaq
Комментарии (3)
shsv382
04.02.2025 04:37Использую pinia, очень удобно, для каждого раздела свой store, плюс встроенное сохранение состояния (persist) с возможностью выбора места хранения - localStorage, sessionStorage. Вообще после реакта вью очень легкий - как будто снял свинцовые сапоги
simple-mortal
Не вдавался в причины, по которым это было убрано, но это был очень хороший инструмент, на мой взгляд. Очень удобно было сажать на шину несколько компонентов, если нужно, чтобы они отслеживали события кастомные. И появление всяких библиотечек говорит о том, что я не один так считаю )
А по поводу остального — работал с пиниа. Прикольная штука. Проще, чем vuex, понятнее. Но чаще юзаю просто реактивный объект небольшой. Как верно было замечено, можно логику упаковывать по разным изолированным объектикам. Единственное, чнемного раздражает — импорты во всех причинных местах и если нужно отрисовать в шаблоне, то через компьютед приходится свойство передавать. Но это мелочи.
zede
Так как это изначально не было целенаправленным функционалом Vue. Скорее случайным образом обнаруженная возможность для реализации. Просто из Vue в целом выпилили идею топорных событий (теперь они заранее регистрируются). Если так подумать то действительно смешно было наблюдать по 5-6 инстансов Vue для того чтоб сообщениями обменивались компоненты, как-то логика нарушается