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

Будет полезен тем кому нравится или приходится работать с 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 - плохая практика.
С чем лично я не согласен.

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

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


  1. Alexufo
    13.07.2025 09:22

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

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

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


    1. matim_ioioi
      13.07.2025 09:22

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

      А где написано, что он не рекомендуется? Просто о нём автор нигде ничего не нашёл в «интернетах»

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

      Иногда (точнее, почти всегда), для DX, а так же, что не маловажно, для оптимизации, лучше разграничить такие компоненты на разные, а не держать все варианты представления в одном компоненте. Это улучшит читаемость кода, его дальнейшние модификации (при надобности) и уберёт лишние пропы (или просто условия внутри компонента) по которым Вы хотите отобразить то или иное его представление


  1. my_username
    13.07.2025 09:22

    -тся


    1. HardMagnett Автор
      13.07.2025 09:22

      Спасибо за ревью. Исправлено )


  1. matim_ioioi
    13.07.2025 09:22

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

    1. Проблема не описана

    2. Пример посредственный. Объясню: то, что можно сделать композабл, который принимает в себя пропы и эмиты какого-либо компонента, использует эти пропы и эмитит эти эмиты и ежу понятно

    3. Не очень понятно, в каких реальных кейсах это может использоваться, так как даже при «однотипной» логики событий (как пример, возьмём различные типы интерактивных элементов формы — текстовый инпут, численный инпут, датапикер и прочие, где есть один и тот же эмит «изменения текущего значения») — логика внутри абсолютно разная, а вот зарывать обработки всех разных типов в один «общий» композабл — это оверхед, так как проще (и для чтения кода, и для рантайма, и для будущих модификаций кода, и для последующего рефакторинга при надобности) обособить ответственность конкретного компонента именно в нём

    4. Но, если уж представить, что нам прям хочется сделать что-то такое универсальное, то использование эмита в описанном примере не совсем корректное. Если уж Вы решаете проблему «переиспользования одинаково названных и по сути дублирующих логику эмитов», то недостаточно описать тип эмита в самом композабле — как минимум понадобится дженерик, т.к. такие эмиты могут «выбрасывать» разные типы данных, как максимум — всё-таки придётся дать возможность прокидывать свой обработчик на данное событие, так как логика в каком-то компоненте может быть нетривиальна, а нам же захочется его поместить в эту нами созданную «универсальную бутылку» (не, ну а зачем тогда мы это делали, верно же?). Но тут опять же поинт про то, что это усложнение кодовой базы, которое в будущем вряд ли даст нам профит — скорее наоборот, принесёт проблем, когда нужно будет что-то изменить в одном из типов компонентов (либо придётся обработку выносить на верхний уровень — в сам компонент (но зачем мы тогда всё это делали, опять же?), либо изменять реализацию уже в самом композабле — что будет довольно проблематично, так как его уже будут использовать несколько компонентов). В итоге получаем некий «вендор лок», который станет блокером для некоторых задач и потребует ресурсов на устранение созданных нами проблем в рамках всеми любимого «техдолга»

    Возможно, в чём-то я не прав, что-то не понял из Вашей статьи и так далее и тому подобное. Основной мой поинт в чём: из статьи не понятна проблема, предложенное решение соответственно не понятно, какую проблему решает, так как проблема явно не описана, а пример уж очень поверхностный, что из него так же не становится ясна проблема

    Если бы Вы добавили пример реального использования (да, прям реального использования из коммерческой разработки) — возможно, это бы всё расставило на свои места

    В любом случае, спасибо за статью!

    UPD: если Вы имели ввиду компоненты, которые действительно делают прям одно и то же, но просто имеют разное представление (да, да, такое действительно бывает) — в таком случае всё встаёт на свои места. Кроме одного: почему у статьи уровень «сложный»?)


    1. HardMagnett Автор
      13.07.2025 09:22

      Спасибо за развернутый коммент.

      UPD: если Вы имели ввиду компоненты, которые действительно делают прям одно и то же, но просто имеют разное представление (да, да, такое действительно бывает) — в таком случае всё встаёт на свои места.

      Да, я писал в точности об этом.


      Реальную проблему из жизни, на основе которой писал статью, не стал описывать по 2-м причинам:
      - Нехорошо код из продакшн-проекта, находящийся под NDA, публиковать на всеобщее обозрение.
      - Хотелось сделать минимальный пример, который я или кто-то другой может скопировать в шпаргалки или сниппеты, и в следующий раз не выдумывать заново. Исходя из моего опыта - это периодически повторяющийся шаблон.


      1. matim_ioioi
        13.07.2025 09:22

        Да, я писал в точности об этом.

        Тогда, как я и написал, всё встаёт на свои места :)

        Нехорошо код из продакшн-проекта, находящийся под NDA, публиковать на всеобщее обозрение

        Ну, никто не говорит прям брать код из продакшна проекта под NDA и сувать его сюда. Можно взять юзкейс из него и описать пример со всеми вытекающими. Да, и, на самом деле, Вы бы в любом случае полностью весь код с бизнес-логикой и тд не вставляли, а вставили части, которые относятся непосредственно к сути данной статьи. NDA может быть, конечно, всякий, но, думаю, никто бы не был против этого, если уж так говорить :)

        Думаю, многим было бы интересно узнать, в каких именно реальных кейсах это можно использовать с пользой, а не во вред. Но это Ваше право, как автора статьи

        Спасибо за ответ!


    1. HardMagnett Автор
      13.07.2025 09:22

      Почему уровень "сложный"?

      Насколько я понял:

      • "простой" - нужно ставить, если целевая аудитория - не программисты вовсе,

      • "средний" - для около-программерских специальностей, которые имеют примерное представление о кодинге, но сами непосредственно им редко занимаются,

      • "сложный" - непосредственно для кодеров, которые знают тонкости языков, фреймворков, инструментов и прочего.

      Нечто подобное было написано в интерфейсе хабра при публикации.

      Разве нет?

      Может-быть и я что-то не так понял.
      Это моя первая статья на Хабр.


      1. matim_ioioi
        13.07.2025 09:22

        Возможно

        Честно говоря, не знаю, как по правилам хабра (я пока не писал тут статей, поэтому не изучал этот вопрос), просто выразил своё мнение по «среднему по больнице» относительно кучи других мной прочитанных статей. Видимо, тут каждый выставляет как хочет :)