javascript

Как правильно реализовать кнопку «Назад» во Vue: просто о сложном

  • четверг, 16 апреля 2026 г. в 00:00:09
https://habr.com/ru/articles/1023578/

Сегодня разберём тему, которая кажется элементарной, но на практике вызывает кучу вопросов. Речь о кнопке «Назад» в приложении на Vue.

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

Немного жизни из собеседований

Когда на интервью я спрашиваю: «Как вы реализуете переход назад?», в 90% случаев слышу уверенное: router.push(). Спасибо тем, кто хотя бы вспоминает про router.go(-1) - таких меньшинство.

Проблема в том, что router.push() - это не про «назад». Это про «вперёд, с записью в историю». И если использовать его для кнопки «Назад», мы получаем классический конфликт механизмов навигации.

Два типа навигации: в чём разница?

Давайте разложим по полочкам:

Метод

Что делает

Что происходит с историей

router.push()

Переходит по указанному маршруту

Добавляет новую запись в стек истории

router.back() / router.go(-1)

Возвращает на шаг назад

Перемещает указатель по существующей истории, не создавая новых записей

Когда вы вешаете на кнопку «Назад» обработчик с router.push(), вы подменяете одно действие другим. Браузер думает, что пользователь хочет вернуться, а приложение говорит: «А давай-ка я тебе новую страницу добавлю».

Что происходит на практике: сценарий проблемы

Представим простую цепочку переходов:

[Главная] → [Список товаров] → [Товар А](мы сейчас здесь)

✅ Правильное поведение (через router.back())

  1. Пользователь нажимает вашу кнопку «Назад»

  2. Срабатывает router.back() или router.go(-1)

  3. Указатель истории сдвигается на одну позицию назад

[Главная] → [Список товаров] ← (текущая)

Пользователь видит страницу «Список товаров». Всё как он и ожидал.

❌ Неправильное поведение (через router.push())

  1. Пользователь нажимает кнопку «Назад»

  2. Срабатывает router.push({ name: 'product-list' })

  3. Vue Router видит, что «Список товаров» уже есть в истории, но поскольку это push - он добавляет дубль

[Главная] → [Список товаров] → [Товар А] → [Список товаров (дубль!)] (теперь мы здесь)

Указатель истории оказывается в конце, на новой, дублирующей записи.

❔К чему это приводит: «ловушка истории»

Теперь представьте, что пользователь, оказавшись на дубле «Списка товаров», решит нажать кнопку «Назад» в браузере:

  1. История: [...Товар А] → [Список товаров (дубль)] ← текущая

  2. Нажатие «Назад» в браузере возвращает его на [Товар А]

  3. Он снова видит ваш кастомный бэк-кнопку → снова 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>

Что здесь важно

  1. beforeNavigate - позволяет показать модалку «Вы уверены?» или сохранить черновик перед уходом. Возврат false или Promise.resolve(false) отменяет переход.

  2. isSamePage - ваша персональная логика, которая помогает понять: пользователь «гуляет» внутри одного раздела или пришёл извне. Если внутри - безопасно делать back(), если снаружи - лучше уйти на известный fallbackRoute.
    Важно - это ваш персональный блок с вашей логикой. Он у вас будет другой!

  3. fallbackRoute - страховка на случай, когда истории нет (прямой заход, обновление страницы).

  4. beforeRouterPush - «аварийный выход» для совсем кастомных сценариев, когда стандартная логика не подходит.

🚀 Все просто

Кнопка «Назад» - это не просто стрелочка в интерфейсе. Это контракт с пользователем: «ты нажал - я верну тебя туда, откуда ты пришёл, и не сломаю навигацию».

Используйте router.back(), думайте о кривых случаях, и ваше приложение будет вести себя так, как ожидает пользователь. А это - половина успеха в юзабилити.