Как происходит обмен данными между sibling-компонентами?

Обмен данными между sibling-компонентами (компонентами-«братьями», находящимися на одном уровне в иерархии) во Vue напрямую невозможен, так как Vue использует однонаправленный поток данных: родитель передаёт данные дочерним компонентам через props, а дочерние сообщают об изменениях через events. Однако обмен между sibling-компонентами реализуется через один из следующих механизмов:

1. Через общего родителя

Самый стандартный способ — использование общего родительского компонента, который передаёт данные обоим детям и следит за их изменениями.

Пример структуры:

Parent
├── ChildA
└── ChildB
**Parent.vue**
<template>
<ChildA @update-value="handleUpdate" />
<ChildB :sharedValue="sharedValue" />
</template>
<script setup>
import { ref } from 'vue';
import ChildA from './ChildA.vue';
import ChildB from './ChildB.vue';
const sharedValue = ref('');
function handleUpdate(newValue) {
sharedValue.value = newValue;
}
</script>
**ChildA.vue**
<template>
<input @input="emitValue" />
</template>
<script setup>
import { defineEmits } from 'vue';
const emit = defineEmits(\['update-value'\]);
function emitValue(event) {
emit('update-value', event.target.value);
}
</script>
**ChildB.vue**
<template>
<p>Получено: {{ sharedValue }}</p>
</template>
<script setup>
defineProps({
sharedValue: String
});
</script>

Таким образом, ChildA отправляет данные родителю через emit, а родитель передаёт их ChildB через props.

2. Через глобальное хранилище (например, Pinia)

Если компоненты не имеют общего родителя или находятся на разных уровнях, можно использовать глобальное состояние, например через Pinia.

**store/counter.js**
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
value: ''
}),
actions: {
updateValue(newVal) {
this.value = newVal;
}
}
});
**ChildA.vue**
<script setup>
import { useCounterStore } from '@/store/counter';
const store = useCounterStore();
function handleInput(event) {
store.updateValue(event.target.value);
}
</script>
<template>
<input @input="handleInput" />
</template>
**ChildB.vue**
<script setup>
import { useCounterStore } from '@/store/counter';
const store = useCounterStore();
</script>
<template>
<p>{{ store.value }}</p>
</template>

Pinia автоматически делает store.value реактивным, и ChildB обновляется при изменении значения в ChildA.

3. Через provide/inject

Можно использовать provide и inject даже между sibling-компонентами, если их общий родитель предоставляет нужные зависимости.

**Parent.vue**
<script setup>
import { provide, ref } from 'vue';
const sharedValue = ref('');
provide('sharedValue', sharedValue);
</script>
<template>
<ChildA />
<ChildB />
</template>
**ChildA.vue**
<script setup>
import { inject } from 'vue';
const sharedValue = inject('sharedValue');
function updateValue(event) {
sharedValue.value = event.target.value;
}
</script>
<template>
<input @input="updateValue" />
</template>
**ChildB.vue**
<script setup>
import { inject } from 'vue';
const sharedValue = inject('sharedValue');
</script>
<template>
<p>{{ sharedValue }}</p>
</template>

Такой подход работает, потому что ref сохраняет реактивность при передаче через provide.

4. Через event bus (Vue 2)

В Vue 2 использовался так называемый "event bus" — отдельный экземпляр Vue, на котором можно было подписываться на события и отправлять их между компонентами.

// bus.js
import Vue from 'vue';
export const EventBus = new Vue();
**ChildA.vue (Vue 2)**
import { EventBus } from './bus';
EventBus.$emit('some-event', 'данные');
**ChildB.vue (Vue 2)**
import { EventBus } from './bus';
EventBus.$on('some-event', data => {
this.receivedData = data;
});

Этот способ считался антипаттерном и был исключён из Vue 3.

5. Через emits + v-model + sync (Vue 2)

В Vue 2 существовал v-bind.sync, позволяющий сократить передачу данных между детьми и родителем. В Vue 3 это заменено на v-model с аргументами.

**Parent.vue**
<template>
<ChildA v-model:value="shared" />
<ChildB :value="shared" />
</template>
<script setup>
import { ref } from 'vue';
const shared = ref('');
</script>
**ChildA.vue**
<script setup>
const props = defineProps(\['value'\]);
const emit = defineEmits(\['update:value'\]);
function onInput(event) {
emit('update:value', event.target.value);
}
</script>
<template>
<input :value="value" @input="onInput" />
</template>
**ChildB.vue**
<script setup>
const props = defineProps(\['value'\]);
</script>
<template>
<p>{{ value }}</p>
</template>

Это позволяет упростить синхронизацию данных между компонентами через родителя.

6. Через mitt (аналог event bus для Vue 3)

Для Vue 3 можно использовать стороннюю библиотеку mitt как более безопасную и современную альтернативу event bus.

// mitt.js
import mitt from 'mitt';
export const emitter = mitt();
**ChildA.vue**
import { emitter } from './mitt';
emitter.emit('send', 'новое значение');
**ChildB.vue**
import { emitter } from './mitt';
onMounted(() => {
emitter.on('send', data => {
value.value = data;
});
});

mitt — лёгкая библиотека для событийной коммуникации между любыми частями приложения.

7. Через slots + scoped slots

Иногда можно передать функцию или данные через слоты, особенно когда один компонент управляет отображением других.

<Parent>
<template #default="{ value, update }">
<ChildA :onChange="update" />
<ChildB :value="value" />
</template>
</Parent>

Такой паттерн требует настройки слотов и используется реже, но возможен в сложных компонентах, например библиотеках компонентов.

Эти способы позволяют реализовать обмен данными между sibling-компонентами в зависимости от архитектуры приложения, уровня вложенности и требований к масштабируемости.