логотип PurpleSchool
логотип PurpleSchool

Загрузка и управление состоянием загрузки в Vue

Автор

Олег Марков

Введение

В любом современном фронтенд-приложении часто требуется загружать данные с сервера или выполнять другие асинхронные операции. Если вы разрабатываете на Vue, важно не только реализовать саму загрузку, но и сообщать пользователю о ходе процесса: показывать спиннеры, skeleton-структуры, блокировать ввод или кнопки на время ожидания результата. Для этого используется концепция состояния загрузки (loading state).

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

Виды состояний загрузки

Перед тем как приступить к практике, давайте разберемся, какие могут быть типы состояний загрузки в вашем проекте.

Локальная загрузка

Часто бывает, что загрузка данных касается одного компонента, где показывается спиннер или skeleton именно для части UI.

  • Пример: загрузка информации профиля пользователя.

Глобальная загрузка

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

  • Пример: overlay на весь экран или индикатор в шапке сайта.

Загрузка нескольких ресурсов

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

  • Пример: одновременно запрашиваете список пользователей и их сообщения.

Теперь давайте перейдём к практическим примерам.

Организация состояния загрузки в одиночном компоненте

Самый базовый способ — создать реактивную переменную, обозначающую загрузку, и использовать её в шаблоне компонента.

Пример реализации

<template>
  <div>
    <button @click="fetchData" :disabled="loading">
      Загрузить данные
    </button>
    <div v-if="loading">
      Загрузка...
    </div>
    <div v-else-if="error">
      Ошибка: {{ error }}
    </div>
    <div v-else-if="data">
      <pre>{{ data }}</pre>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      loading: false, // Показывает, идёт ли загрузка
      data: null,     // Здесь будут полученные данные
      error: null     // Сюда запишется текст ошибки
    }
  },
  methods: {
    async fetchData() {
      this.loading = true
      this.error = null
      try {
        // Здесь - типовой async запрос
        const response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
        if (!response.ok) throw new Error('Network error')
        this.data = await response.json()
      } catch (e) {
        // Если ошибка, записываем её
        this.error = e.message
      } finally {
        this.loading = false // В любом случае выключаем индикатор
      }
    }
  }
}
</script>

В этом коде вы видите классическую схему: индикатор загрузки (loading) активируется при запуске загрузки, отключается при завершении (в блоке finally).

Особенности управления асинхронностью

Важно всегда отключать loading внутри блока finally, чтобы он выключался независимо от результата (успех или ошибка). Это избежит "залипания" индикатора.

Реализация Skeleton-структур

Вместо слова "Загрузка..." большинство современных интерфейсов используют специальные скелетоны — пустые блоки с анимацией, имитирующие контент.

Как реализовать skeleton в Vue

Подключите css из библиотеки или используйте элементарный стиль:

<template>
  <div v-if="loading" class="skeleton skeleton-title"></div>
  <div v-else>
    <h2>{{ data.title }}</h2>
  </div>
</template>

<style>
.skeleton {
  background: #eee;
  border-radius: 4px;
  min-height: 1em;
  animation: skeleton-loading 1.2s infinite linear;
}
.skeleton-title {
  width: 240px;
  height: 28px;
  margin-bottom: 16px;
}
@keyframes skeleton-loading {
  0% {background-color: #eee;}
  50% {background-color: #ddd;}
  100% {background-color: #eee;}
}
</style>

Вы можете использовать любые специализированные библиотеки skeleton (Vuetify Skeleton Loader, vue-loading-skeleton и др.) или создать свои css-классы, как на примере.

Использование Composition API для состояния загрузки

С приходом Vue 3 удобнее стало выносить логику загрузки в переиспользуемые функции. Например, вы можете сделать кастомный composition function и применять его в разных компонентах. Смотрите, как это выглядит:

// useFetch.js — кастомный composable
import { ref } from 'vue'

export function useFetch(url) {
  const loading = ref(false)
  const data = ref(null)
  const error = ref(null)
  
  const fetchData = async () => {
    loading.value = true
    error.value = null
    try {
      const response = await fetch(url)
      if (!response.ok) throw new Error('Network error')
      data.value = await response.json()
    } catch (e) {
      error.value = e.message
    } finally {
      loading.value = false
    }
  }
  
  return { loading, data, error, fetchData }
}

Теперь интегрируем его в компонент:

<script setup>
import { useFetch } from './useFetch.js'

const { loading, data, error, fetchData } = useFetch('https://jsonplaceholder.typicode.com/posts/1')

// Запускаем загрузку при монтировании
fetchData()
</script>

<template>
  <div>
    <div v-if="loading">Загрузка...</div>
    <div v-else-if="error">Ошибка: {{ error }}</div>
    <div v-else><pre>{{ data }}</pre></div>
  </div>
</template>

Теперь всю механику можно внедрить в любой компонент, а при необходимости менять URL.

Управление глобальным состоянием загрузки

Если вы хотите показывать единый лоадер на весь экран, или инициировать/останавливать его из разных мест приложения, удобнее использовать глобальное состояние — например, Vuex (Vue 2), Pinia (Vue 3) или provide/inject.

Рассмотрим на примере Pinia:

Создаем хранилище (store) для загрузки

// stores/useLoadingStore.js
import { defineStore } from 'pinia'

export const useLoadingStore = defineStore('loading', {
  state: () => ({
    active: false,       // Показывает, идет ли сейчас какой-либо процесс
    counter: 0           // Для подсчета параллельных загрузок
  }),
  actions: {
    startLoading() {
      this.counter++
      this.active = true
    },
    stopLoading() {
      this.counter--
      if (this.counter <= 0) {
        this.counter = 0
        this.active = false
      }
    }
  }
})

Такой подход позволяет не "отключать" глобальный лоадер случайно, если одновременно запущено несколько загрузок. Индикатор исчезнет только тогда, когда counter станет равен нулю.

Используем глобальный индикатор

<!-- App.vue -->
<template>
  <div>
    <LoadingOverlay v-if="loading.active" />
    <router-view />
  </div>
</template>

<script setup>
import { useLoadingStore } from './stores/useLoadingStore'
import { storeToRefs } from 'pinia'
import LoadingOverlay from './components/LoadingOverlay.vue'

const loading = storeToRefs(useLoadingStore())
</script>

В вызовах асинхронных действий теперь не забудьте:

import { useLoadingStore } from '../stores/useLoadingStore'

const loadingStore = useLoadingStore()

async function fetchData() {
  loadingStore.startLoading()
  try {
    await fetch('...')
  } finally {
    loadingStore.stopLoading()
  }
}

Управление загрузкой для нескольких параллельных запросов

Бывают ситуации, когда нужно видеть состояние каждой загрузки отдельно, или наоборот — все вместе. Для этого удобно использовать мапу или объект с ключами.

Пример с Map

import { reactive } from 'vue'

const loadingMap = reactive({})

// Перед запуском запроса
loadingMap['users'] = true
loadingMap['messages'] = true

// Когда загрузка завершена
loadingMap['users'] = false

// В шаблоне
<div v-if="loadingMap['users']">Загрузка пользователей...</div>
<div v-if="loadingMap['messages']">Загрузка сообщений...</div>

Это особенно полезно в больших списках или когда запросы динамические.

Показываем индикатор только при долгой загрузке

Частая UX-ошибка — показывать спиннер при каждом запросе, даже если он занимает миллисекунды. Лучшее решение — немного задерживать отображение индикатора, чтобы мелкие запросы не мигали.

data() {
  return {
    loading: false,
    timeoutId: null
  }
},
methods: {
  async fetchData() {
    this.timeoutId = setTimeout(() => {
      this.loading = true          // Показываем спиннер только если прошло больше 400мс
    }, 400)
    try {
      await fetch('...')
    } finally {
      clearTimeout(this.timeoutId)
      this.loading = false         // Гасим спиннер сразу после завершения
    }
  }
}

Этот трюк делает взаимодействие с интерфейсом более "гладким", без лишних миганий.

Дизайн кнопок и блокировка UI во время загрузки

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

<button @click="save" :disabled="loading">
  <span v-if="loading" class="spinner"></span>
  Сохранить
</button>

Такой подход предотвратит дублирующие клики.

Ошибки и повторные попытки загрузки

Не забывайте о возможности ошибки или неудачного запроса. Добавьте UI для повторной попытки и отображайте, какое именно действие прошло неудачно.

<div v-if="error">
  <p>Не удалось загрузить данные: {{ error }}</p>
  <button @click="fetchData">Повторить</button>
</div>

Это минимальное требование для любого серьезного приложения.

Использование сторонних библиотек загрузки

Существуют готовые решения для управления состоянием загрузки или красивых индикаций:

Плюс таких решений — быстрая интеграция, минус — меньше гибкости под вашу логику.

Заключение

Организация состояния загрузки — неотъемлемая часть пользовательского опыта во Vue-приложениях. Вы можете реализовать локальное управление для небольших компонентов, вынести логику в переиспользуемый composable или использовать глобальный стор (Pinia/Vuex), если нужно отслеживать асинхронные процессы централизованно. Не забывайте про skeleton-заглушки и обработку ошибок, чтобы интерфейс был современным и информативным. Оптимизируйте UX, показывая индикатор только при действительно долгих операциях и блокируя UI, где это необходимо.

Грамотное управление loading state поможет сделать ваше приложение быстрым и дружелюбным для пользователей.

Частозадаваемые технические вопросы по теме статьи и ответы на них

Как показывать индикатор загрузки только для определенного запроса, если на странице много асинхронных загрузок?

Используйте независимые реактивные переменные или объект (map), где у каждой загрузки свой ключ (например, loadingComments, loadingPosts). Вы также можете применить reactive-объект: js const loading = reactive({ posts: false, comments: false }) В шаблоне подключайте нужный индикатор к конкретной переменной.

Как правильно обрабатывать отмену асинхронных запросов при смене компонента или маршрута?

Используйте AbortController для fetch-запросов, а в Composition API сбрасывайте состояния компонентов в onUnmounted или onBeforeUnmount. Это предотвращает появление "утекших" загрузочных процессов и ошибок, возникающих после размонтирования компонента.

Что делать, если лоадер не исчезает после загрузки?

Возможно, промис (или fetch) не завершился, ошибка не отловлена, или отсутствует блок finally, выключающий loading. Проверьте, завершается ли асинхронная функция, и убедитесь, что очистка состояния всегда происходит даже при ошибках.

Как комбинировать глобальный loading с локальными состояниями загрузки?

Можно в компонентах вызывать глобальные методы управления лоадером (например, через Pinia), а локальные — для компонентов, где это действительно нужно. Такой подход часто комбинируется для разделения scope-индикации (внутри блока/экрана и на уровне всего приложения).

Можно ли автоматически показывать лоадер при переходах между страницами в Vue Router?

Да, используйте хуки beforeEach и afterEach в роутере для активации/деактивации глобального loading:

router.beforeEach((to, from, next) => {
  loadingStore.startLoading()
  next()
})
router.afterEach(() => {
  loadingStore.stopLoading()
})

Это решение автоматически покажет индикатор при долгих переходах с загрузкой данных.

Стрелочка влевоИспользование query-параметров и их обработка в маршрутах VueИспользование библиотек Vue для расширения функционалаСтрелочка вправо

Все гайды по Vue

Руководство по валидации форм во Vue.jsИнтеграция Tiptap для создания редакторов на VueРабота с таблицами во Vue через TanStackИнструкция по установке и компонентам Vue sliderУправление пакетами Vue js с помощью npmУправление пакетами и node modules в Vue проектахКак использовать meta для улучшения SEO на VueПолный гайд по компоненту messages во Vuejs5 правил использования Inertia с Vue и LaravelРабота с модулями и пакетами в VueИнструкция по работе с grid на VueGithub для Vue проектов - подробная инструкция по хранению и совместной работеНастройка ESLint для Vue проектов и поддержка качества кодаОбработка ошибок и отладка в Vue.jsИспользование Vue Devtools для отладки и мониторинга приложенийРабота с конфигурационными файлами и скриптами VueСоздание и настройка проектов Vue с помощью Vue CLI3 способа интеграции Chart.js с Vue для создания графиковРабота с Canvas во VueИнструкция по реализации календаря во VueРабота с Ant Design Vue для создания UI на Vue
Обзор и использование утилит Vue для удобной разработкиРабота с обновлениями компонента и жизненным циклом updateРазрешение конфликтов и ошибок с помощью Vue resolveИспользование query-параметров и их обработка в маршрутах VueЗагрузка и управление состоянием загрузки в VueИспользование библиотек Vue для расширения функционалаРабота с JSON данными в приложениях VueКак работать с экземплярами компонента Instance во VueПолучение данных и API-запросы во Vue.jsЭкспорт и импорт данных и компонентов в VueОбработка событий и их передача между компонентами VuejsГайд по defineEmits на Vue 3Понимание core функционала Vue и его применениеПонимание и применение Composition API в Vue 3Понимание и работа с компилятором VueКогда и как использовать $emit и call во VueВзаимодействие с внешними API через Axios в Vue
Веб приложения на Vue архитектура и лучшие практикиИспользование Vite для быстрого старта и сборки проектов на Vue 3Работа с URL и ссылками в приложениях на VueРабота с пользовательскими интерфейсами и UI библиотеками во VueОрганизация и структура исходных файлов в проектах VueИспользование Quasar Framework для разработки на Vue с готовыми UI-компонентамиОбзор популярных шаблонов и стартовых проектов на VueИнтеграция Vue с PHP для создания динамичных веб-приложенийКак организовать страницы и маршруты в проекте на VueNuxt JS и Vue 3 для SSR приложенийСоздание серверных приложений на Vue с помощью Nuxt jsИспользование Vue Native для разработки мобильных приложенийОрганизация и управление индексной страницей в проектах VueИспользование Docker для контейнеризации приложений на VueИнтеграция Vue.js с Django для создания полноценных веб-приложенийСоздание и работа с дистрибутивом build dist Vue приложенийРабота со стилями и CSS в Vue js для красивых интерфейсовСоздание и структурирование Vue.js приложенияКак исправить ошибку cannot find module vueНастройка и сборка проектов Vue с использованием современных инструментовИнтеграция Vue с Bitrix для корпоративных решенийРазработка административных панелей на Vue js
5 библиотек для создания tree view во VueИнтеграция Tailwind CSS с Vue для современных интерфейсовИнтеграция Vue с серверной частью и HTTPS настройкамиКак обрабатывать async операции с Promise во VueИнтеграция Node.js и Vue.js для разработки приложенийРуководство по интеграции Vue js в NET проектыПримеры использования JSX во VueГайд по импорту и регистрации компонентов на VueМногоязычные приложения на Vue с i18nИнтеграция FLIR данных с Vue5 примеров использования filter во Vue для упрощения разработки3 примера реализации drag-and-drop во Vue
Управление переменными и реактивными свойствами во VueИспользование v for и slot в VueПрименение v-bind для динамической привязки атрибутов в VueУправление пользователями и их данными в Vue приложенияхСоздание и использование UI Kit для Vue приложенийТипизация и использование TypeScript в VuejsИспользование шаблонов в Vue js для построения интерфейсовИспользование Swiper для создания слайдеров в VueРабота со стилями и стилизацией в VueСтруктура и особенности Single File Components SFC в VueРабота со SCSS в проектах на Vue для стилизацииРабота со скроллингом и прокруткой в Vue приложенияхПрименение script setup синтаксиса в Vue 3 для упрощения компонентовИспользование scoped стилей для изоляции CSS в компонентах Vue3 способа улучшить навигацию Vue с push()Обработка запросов и асинхронных операций в VueПонимание и использование provide inject для передачи данных между компонентамиПередача и использование props в Vue 3 для взаимодействия компонентовПередача данных между компонентами с помощью props в Vue jsУправление property и функциями во Vue.jsРабота со свойствами компонентов VueУправление параметрами и динамическими данными во VueРабота с lifecycle-хуком onMounted во VueОсновы работы с объектами в VueПонимание жизненного цикла компонента Vue js на примере mountedИспользование модальных окон modal в Vue приложенияхИспользование методов в компонентах Vue для обработки логикиИспользование метода map в Vue для обработки массивовИспользование хуков жизненного цикла Vue для управления состоянием компонентаРабота с ключами key в списках и компонентах VueОбработка пользовательского ввода в Vue.jsРабота с изображениями и их оптимизация в VueИспользование хуков жизненного цикла в VueОрганизация сеток и гридов для верстки интерфейсов на VueСоздание и управление формами в VueОрганизация файлов и структура проекта Vue.jsКомпоненты Vue создание передача данных события и emitРабота с динамическими компонентами и данными в Vue3 способа манипулирования DOM на VueРуководство по div во VueИспользование директив в Vue и их расширенные возможностиОсновы и применение директив в VueИспользование директив и их особенности на Vue с помощью defineИспользование компонентов datepicker в Vue для выбора датОрганизация циклов и итераций во VueКак работает компиляция Vue CoreСоздание и использование компонентов в Vue JSОбработка кликов и пользовательских событий в VueИспользование классов в Vue для организации кода и компонентовИспользование директивы checked для управления состоянием чекбоксов в VueГайд на checkbox компонент во VueОтображение данных в виде графиков с помощью Vue ChartСоздание и настройка кнопок в VueСоздание и настройка кнопок в Vue приложенияхРабота с lifecycle-хуками beforeCreate и beforeMount во VueИспользование массивов и методов их обработки в VueИспользование массивов и их обработка в Vue
Использование Vuetify для создания современных интерфейсов на VueИспользование transition во VueТестирование компонентов и приложений на VueРабота с teleport для управления DOM во VueПять шагов по настройке SSR в VuejsИспользование Shadcn UI компонентов с Vue для продвинутых интерфейсовИспользование router-link для навигации в Vue RouterКак использовать require в Vue для динамического импорта модулейРабота с динамическим рендерингом и виртуальным DOM на Vue.jsИспользование ref для управления ссылками и реактивностью в Vue 3Использование Vue Pro и его преимущества для профессиональной разработкиРуководство по nextTick для работы с DOMСоздание и использование компонентов с помощью Vue js и CУправление состоянием и реактивностью через inject и provideДинамическое обновление компонентов и данных на VueГлубокое изучение документации Vue и как эффективно её использоватьИспользование Crystal с Vue для разработкиИспользование вычисляемых свойств для динамического отображения данных на Vue jsОптимизация производительности и предупреждения в Vue
Открыть базу знаний