Пришел к хитрому паттерну. Делюсь.

Будет полезен тем кому нравиться или приходится работать с Vue.
В подходящей ситуации он сэкономит кучу времени и поможет избежать дублирования кода.

Контекст

  • Есть несколько компонентов.

  • Компоненты должны выполнять одинаковую сложную логику.

  • Компоненты должны принимать одинаковые свойства и эмитить одинаковые события.

  • Свойства из composable могут быть опциональными со значениями по умолчанию.

  • Каждый компонент выглядит совершенно по разному (разная вёрстка).

  • Каждый компонент, опционально, в дополнение к общему, может:

    • принимать собственные свойства

    • эмитить собственные события

    • выполнять дополнительную логику.

Пожелания к реализации

  • Избежать дублирования событий и свойств.

  • Избежать дублирования логики.

Проблематика : Composables vs Mixins

Раньше, во Vue2 без TS и с mixin-ами вместо composables,
можно было легко вынести логику (функции-методы) в mixin,
описать в этом mixin-е общие свойства,
события вообще не нужно было типизировать - их достаточно было просто эмитить.
Потом этот mixin нужно было подключить к компоненту, и вуаля! Всё работает.
В чем-то похоже на mixin-ы из классического ООП.

Теперь, во Vue3 + TS + composables
нужно хитро жонглировать типами,
чтобы добиться поведения, схожего с тем,
которое можно было реализовать на Vue2 mixin-ами без типизации.

Минималистичный пример решения проблемы

// composable useSomething.ts

// Объявляем и экспортируем типы общих свойств...
export type Props = {
  propFromSomething?: string
}
// ... значения по умолчанию для общих свойств ...
export const DEFAULT_PROPS = {
  propFromSomething: 'bar',
}
// ... и общие события.
export type Emit = {
  // Трюк с передачей эмитов в composable работает с такой сигнатурой.
  // Таким-же способом нужно описывать события и в компоненте который использует этот composable. 
  (event: 'eventFromSomething', payload: string): void;
  // Например так не работает:
  // eventFromSomething: [value: string]
}

// Вышеописанные типы ни в какой файл не выносим,
// храним в том же файле, что и сам composable.
// Потому что они будут нужны лишь в самом composable, и в тех компонентах, 
// в которых применяется данный composable.

export function useSomething({ props, emit }: {
  // Props из компонента при помощи intersection включает в себя Props из composable,
  // поэтому с типизацией всё в порядке.
  // Required, потому что опциональные свойства заменятся дефолтовыми при передаче,
  // и все будут заполнены значениями.
  props: Required<Props>,
  // emit из компонента при помощи intersection включает в себя Emit из composable,
  // поэтому с типизацией всё в порядке.
  emit: Emit,
}) {
  async function methodFromSomething() {
    emit('eventFromSomething', props.propFromSomething)
  }

  return {
    methodFromSomething
  }
}
// Component.ts
import {
  useSomething,
  type Props as PropsFromSomething,
  DEFAULT_PROPS as DEFAULT_PROPS_FROM_SOMETHING,
  type Emit as EmitFromSomething,
} from "./useSomething";

// Добавляем к свойствам компонента свойства из composable.
export type Props = {
  propFromComponent?: string
} & PropsFromSomething

// Оттуда-же добавляем дефолтовые значения.
const props = withDefaults(defineProps<Props>(), {
  propFromComponent: 'foo',
  ...DEFAULT_PROPS_FROM_SOMETHING
})

// И добавляем к событиям компонента события из composable.
const emit = defineEmits<{
  (event: 'eventFromComponent', payload: boolean): void;
} & EmitFromSomething>()

const { methodFromSomething } = useSomething({ props, emit })

В приведенном минималистичном примере все выглядит просто,
но дойти до этого было сложнее.

Ничего подобного я не видел ни в руководстве по Vue, ни в обучающих материалах.
Комплексный универсальный пример не могли предложить ни StackOverflow, ни нейросети.

В документации по Vue вообще написано что эмитить события из composable - плохая практика.
С чем лично я не согласен.

Пришлось искать решение самостоятельно.

Комментарии (2)


  1. Alexufo
    13.07.2025 09:22

    так если этот подход не рекомендуется, детали то в чем?

    И если вопрос только в вёрстке, почему нельзя было всю ее засунуть в компонент?

    Это же логично, логика одна, представление разное


  1. my_username
    13.07.2025 09:22

    -тся