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

Когда на интервью я спрашиваю: «Как вы реализуете переход назад?», в 90% случаев слышу уверенное: router.push(). Спасибо тем, кто хотя бы вспоминает про router.go(-1) - таких меньшинство.
Проблема в том, что router.push() - это не про «назад». Это про «вперёд, с записью в историю». И если использовать его для кнопки «Назад», мы получаем классический конфликт механизмов навигации.
Давайте разложим по полочкам:
Метод | Что делает | Что происходит с историей |
|---|---|---|
| Переходит по указанному маршруту | Добавляет новую запись в стек истории |
| Возвращает на шаг назад | Перемещает указатель по существующей истории, не создавая новых записей |
Когда вы вешаете на кнопку «Назад» обработчик с router.push(), вы подменяете одно действие другим. Браузер думает, что пользователь хочет вернуться, а приложение говорит: «А давай-ка я тебе новую страницу добавлю».
Представим простую цепочку переходов:
[Главная] → [Список товаров] → [Товар А](мы сейчас здесь)
Пользователь нажимает вашу кнопку «Назад»
Срабатывает router.back() или router.go(-1)
Указатель истории сдвигается на одну позицию назад
[Главная] → [Список товаров] ← (текущая)
Пользователь видит страницу «Список товаров». Всё как он и ожидал.
Пользователь нажимает кнопку «Назад»
Срабатывает 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(), думайте о кривых случаях, и ваше приложение будет вести себя так, как ожидает пользователь. А это - половина успеха в юзабилити.