Composables вместо миксинов: как я раскладываю логику по фичам
Миксины нормально жили ровно до третьего миксина на компонент. Потом начинались вопросы, откуда взялся loading и почему mounted вызывается четыре раза.
Был у меня проект на Vue 2, где на одном компоненте висело три миксина. Работало. Пока коллега не поправил свой миксин и не сломал чужой поиск, которого не касался.
Полдня искали. Оказалось - две data-функции объявляли loading, и один тихо перетирал другой:
// mixins/pagination.js
export default {
data: () => ({ page: 1, loading: false }),
methods: { next() { this.page++ } },
};
// mixins/search.js
export default {
data: () => ({ query: '', loading: false }), // тот же loading
methods: { next() { /* и метод тоже */ } },
};
Ошибки нет, приложение работает, просто неправильно. Худший вид бага.
Это не мы такие криворукие - в документации Vue недостатки миксинов перечислены прямым текстом: непонятно, откуда что взялось, конфликты имён, неявные связи между самими миксинами.
Как то же самое выглядит сейчас
Composable - это обычная функция. Никакой магии:
// composables/usePagination.ts
export function usePagination(total: Ref<number>, perPage = 20) {
const page = ref(1);
const pages = computed(() => Math.ceil(total.value / perPage));
const canNext = computed(() => page.value < pages.value);
function next() {
if (canNext.value) page.value++;
}
return { page, pages, canNext, next };
}
<script setup lang="ts">
const { page, canNext, next } = usePagination(total);
const { query, results, loading } = useSearch(page);
</script>
Видно, кто откуда пришёл. Нужны два независимых пагинатора на одном экране - вызываешь функцию дважды. С миксинами это было в принципе невозможно.
Четыре вещи, которые я гоняю на ревью
Имя начинается с use. Конвенция официальная, и по имени сразу понятно: внутри реактивность, вызывать надо в setup, а не где попало.
Возвращаем объект, а не массив. С массивом каждый переименовывает по-своему, и через месяц никто не помнит, что там на третьей позиции.
Аргументы принимаем через toValue(). Тогда функции всё равно, что ей передали - число, ref или геттер:
import { toValue, type MaybeRefOrGetter } from 'vue';
export function useUser(id: MaybeRefOrGetter<string>) {
const user = ref<User | null>(null);
watchEffect(async () => {
user.value = await api.getUser(toValue(id));
});
return { user };
}
Мелочь, но избавляет от разговоров «а сюда ref передавать или значение».
Подписался - отпишись.
export function useOnline() {
const online = ref(navigator.onLine);
const update = () => (online.value = navigator.onLine);
window.addEventListener('online', update);
window.addEventListener('offline', update);
onUnmounted(() => {
window.removeEventListener('online', update);
window.removeEventListener('offline', update);
});
return { online };
}
Тут есть подстава, на которой я сам обжёгся: код до первого await выполняется синхронно, а после - уже вне setup(). Если зарегистрировать onUnmounted после await, хук просто не привяжется к компоненту. Молча. Слушатель останется висеть.
Где composable уже не помогает
Composable - это логика одного экрана. Как только состояние нужно трём разным веткам роутера и должно переживать размонтирование - это стор, а не composable. Про эту границу у меня есть отдельная заметка про Pinia.
Второй звоночек - функция на семь аргументов, возвращающая двенадцать полей. Это уже не переиспользуемая логика, это компонент, из которого забыли достать шаблон.
Проверка, которой я пользуюсь: если функцию не получается объяснить одним предложением без «и» - её надо резать. usePagination объясняется. useTableWithFiltersAndExport - нет.
Ссылки
- Composables - официальный гайд, включая соглашения об именовании
- Mixins - там же про их недостатки
toValue()- Хуки жизненного цикла в setup