Сегодня разберём тему, которая кажется элементарной, но на практике вызывает кучу вопросов. Речь о кнопке «Назад» в приложении на 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(), думайте о кривых случаях, и ваше приложение будет вести себя так, как ожидает пользователь. А это - половина успеха в юзабилити.

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


  1. nihil-pro
    15.04.2026 04:37

    Еще ни одному ПО или дезигнеру не удалось меня заставить добавить в интерфейс кнопку «назад». Она уже есть в браузере. В любом. Битва продолжается)


    1. Pnym Автор
      15.04.2026 04:37

      Тут, наверно, зависит ещё от того, какой подход вы используете в разработке роутеров. У вас локально может быть один роутер, а внутри компонента - куча if ))


      1. nihil-pro
        15.04.2026 04:37

        А причем тут вообще роутер? Я про кнопку в браузере. Она уже есть, работает как надо, и ею пользуются. Вы же не делаете в интерфейсе адресную строку, чтобы пользователь вводит туда ссылки на ваш же сайт, а кнопку назад для чего делаете? Я этого не понимаю. Еще я ожидаю, что если она вдруг зачем-то есть, то работает также как в браузере, а в описанным вами сценарии это не так. Если я перешел на страницу товара сразу, например из результатов выдачи Гугла, то при нажатии назад, я хочу вернуться назад, то есть в результаты выдачи Гугла, а не на страницу с вашим каталогом которую я вовсе не посещал.


        1. Pnym Автор
          15.04.2026 04:37

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

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

          А про пример с Google - полностью согласен. Именно поэтому в моём компоненте есть fallbackRoute. Если истории нет - уходим туда, куда логично, а не ломаем ожидания. Так что кнопка не «вместо» браузерной, а «дополнительно» к ней, с чуть более умной логикой под конкретный интерфейс.


    1. ReturnVoid
      15.04.2026 04:37

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

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


      1. nihil-pro
        15.04.2026 04:37

        и что она всегда делает ровно тоже самое что нативная браузерная кнопка

        Я как раз про нативную браузерную кнопку назад и писал, и да, она всегда делает ровно то, что должна.

        В действительности же это далеко не всегда так

        В действительности как раз все так. Браузерная кнопка назад имеет предсказуемое и стабильно поведение.