Сегодня разберём тему, которая кажется элементарной, но на практике вызывает кучу вопросов. Речь о кнопке «Назад» в приложении на Vue.
Казалось бы, что тут сложного? Кликнули — ушли на предыдущую страницу. Но нет. Большинство разработчиков, даже с опытом, не до конца понимают, как устроена навигация в роутерах и как работает история браузера. А это критично, когда речь заходит о предсказуемом поведении приложения.

Немного жизни из собеседований
Когда на интервью я спрашиваю: «Как вы реализуете переход назад?», в 90% случаев слышу уверенное: router.push(). Спасибо тем, кто хотя бы вспоминает про router.go(-1) — таких меньшинство.
Проблема в том, что router.push() — это не про «назад». Это про «вперёд, с записью в историю». И если использовать его для кнопки «Назад», мы получаем классический конфликт механизмов навигации.
Два типа навигации: в чём разница?
Давайте разложим по полочкам:
|
Метод |
Что делает |
Что происходит с историей |
|---|---|---|
|
|
Переходит по указанному маршруту |
Добавляет новую запись в стек истории |
|
|
Возвращает на шаг назад |
Перемещает указатель по существующей истории, не создавая новых записей |
Когда вы вешаете на кнопку «Назад» обработчик с router.push(), вы подменяете одно действие другим. Браузер думает, что пользователь хочет вернуться, а приложение говорит: «А давай-ка я тебе новую страницу добавлю».
Что происходит на практике: сценарий проблемы
Представим простую цепочку переходов:
[Главная] → [Список товаров] → [Товар А](мы сейчас здесь)
✅ Правильное поведение (через router.back())
-
Пользователь нажимает вашу кнопку «Назад»
-
Срабатывает
router.back()илиrouter.go(-1) -
Указатель истории сдвигается на одну позицию назад
[Главная] → [Список товаров] ← (текущая)
Пользователь видит страницу «Список товаров». Всё как он и ожидал.
❌ Неправильное поведение (через router.push())
-
Пользователь нажимает кнопку «Назад»
-
Срабатывает
router.push({ name: 'product-list' }) -
Vue Router видит, что «Список товаров» уже есть в истории, но поскольку это
push— он добавляет дубль
[Главная] → [Список товаров] → [Товар А] → [Список товаров (дубль!)] (теперь мы здесь)
Указатель истории оказывается в конце, на новой, дублирующей записи.
❔К чему это приводит: «ловушка истории»
Теперь представьте, что пользователь, оказавшись на дубле «Списка товаров», решит нажать кнопку «Назад» в браузере:
-
История:
[...Товар А] → [Список товаров (дубль)]← текущая -
Нажатие «Назад» в браузере возвращает его на
[Товар А] -
Он снова видит ваш кастомный бэк-кнопку → снова
router.push()→ снова дубль
Получается бесконечный цикл. Пользователь в ловушке, а вы теряете доверие к продукту.

❤ Решение есть! Просто используйте правильные методы
Для кнопки «Назад» в интерфейсе всегда используйте:
-
router.back()— вернуться на один шаг назад -
router.go(-1)— то же самое
Эти методы работают с существующим стеком истории, а не создают новый. Они соответствуют нативному поведению браузера, и пользователь сможет предсказуемо использовать как ваши кнопки, так и системные.
А как же сложные кейсы?
Жизнь редко бывает идеальной. Иногда «назад» — это не просто шаг в истории. Например:
-
Пользователь зашёл на страницу товара напрямую по ссылке (история пуста)
-
Нужно вернуться не на предыдущую страницу, а на конкретный маршрут
-
Перед уходом надо сохранить данные или показать подтверждение
Вот тут и пригодится чуть более продвинутый подход.
Идеальный код: кнопка «Назад» с защитой от дураков
<script setup lang="ts">import { computed } from 'vue'import { useRouter, useRoute, type RouteLocationRaw } from 'vue-router'const props = defineProps<{ /** * Резервный маршрут: куда идти, если в истории некуда возвращаться * (например, пользователь открыл страницу напрямую) */ fallbackRoute?: RouteLocationRaw /** * Хук перед навигацией. * Может вернуть Promise<boolean> или просто boolean. * Если false — навигация отменится (удобно для подтверждений) */ beforeNavigate?: () => boolean | Promise<boolean> /** * Хук для переопределения логики перехода. * Если передан - стандартная логика не сработает. */ beforeRouterPush?: () => void}>()const router = useRouter()const route = useRoute()/** * Проверяем, находимся ли мы «внутри» одной секции приложения. * Например, если текущий и предыдущий путь начинаются с /catalog/... * Это помогает избежать «вылета» из раздела при частых переходах. */const isSamePage = computed<boolean>(() => { const fromRoute = router.options.history.state?.back if (!fromRoute || typeof fromRoute !== 'string') return false const currentPrefix = route.path.split('/')[1] const fromPrefix = fromRoute.split('/')[1] return currentPrefix === fromPrefix})const goBack = async () => { // 1. Сначала даём шанс отменить переход if (props.beforeNavigate) { const shouldProceed = await props.beforeNavigate() if (!shouldProceed) return } // 2. Если передан кастомный обработчик — делегируем ему if (props.beforeRouterPush) { props.beforeRouterPush() return } // 3. Основная логика if (isSamePage.value) { // Если мы «внутри» раздела — просто идём назад по истории router.back() } else if (props.fallbackRoute) { // Если есть запасной маршрут — используем его void router.push(props.fallbackRoute) } else { // Фолбэк на дефолтную страницу (замените Name на ваш роут) void router.push({ name: 'Name' }) }}</script><template> <q-btn icon="arrow_back" @click="goBack" aria-label="Назад" /></template>
Что здесь важно
-
beforeNavigate— позволяет показать модалку «Вы уверены?» или сохранить черновик перед уходом. ВозвратfalseилиPromise.resolve(false)отменяет переход. -
isSamePage— ваша персональная логика, которая помогает понять: пользователь «гуляет» внутри одного раздела или пришёл извне. Если внутри — безопасно делатьback(), если снаружи — лучше уйти на известныйfallbackRoute.
Важно — это ваш персональный блок с вашей логикой. Он у вас будет другой! -
fallbackRoute— страховка на случай, когда истории нет (прямой заход, обновление страницы). -
beforeRouterPush— «аварийный выход» для совсем кастомных сценариев, когда стандартная логика не подходит.
🚀 Все просто
Кнопка «Назад» — это не просто стрелочка в интерфейсе. Это контракт с пользователем: «ты нажал — я верну тебя туда, откуда ты пришёл, и не сломаю навигацию».
Используйте router.back(), думайте о кривых случаях, и ваше приложение будет вести себя так, как ожидает пользователь. А это — половина успеха в юзабилити.
ссылка на оригинал статьи https://habr.com/ru/articles/1023578/