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

от автора

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

ссылка на оригинал статьи https://habr.com/ru/articles/1023578/