Pinia — глобальное управление состоянием

В этой главе вы научитесь использовать Pinia для управления общими данными между компонентами и страницами и реализуете функцию "избранные статьи".


Зачем нам нужно глобальное управление состоянием?

До сих пор данные передавались между компонентами через props и emit.

Этого достаточно для простых сценариев «родитель-потомок», но это становится неудобным в следующих ситуациях:

  • Состояние избранного должно синхронизироваться между главной страницей и страницей деталей.
  • Состояние входа пользователя должно быть общим для всех страниц.
  • Данные корзины покупок должны передаваться между панелью навигации, страницей товара и страницей оформления заказа.

Передача props по слоямПроблема: если данные из компонента A нужно передать в компонент D, они должны пройти через B и C как через промежуточные звенья, хотя самим промежуточным компонентам эти данные не нужны.

PiniaPinia — это библиотека управления состоянием, официально рекомендованная Vue 3. Она предоставляет глобальный «центр данных», где любой компонент может напрямую читать и изменять данные без послойной передачи.


Установка и регистрация Pinia

Если вы уже выбрали Pinia при создании проекта, пропустите шаги установки.

В противном случае:

$ npm install pinia

Зарегистрируйте её в main.js:

Пример

// Путь к файлу: src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'

const app = createApp(App)

// Создайте экземпляр Pinia и зарегистрируйте его
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.mount('#app')

defineStore — Определение хранилища

Ядро Pinia — этоStoreКаждое Store управляет независимой частью состояния.

Он состоит из трёх частей:

ЧастьНазначениеАналогия
stateДанные StoreДанные / ref компонента
геттерыПроизводные вычисляемые данныеВычисляемые свойства компонента
actionsМетоды для изменения данныхМетоды / функции компонента

Пример

// Путь к файлу: src/stores/useFavoriteStore.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// defineStore('storeName', () => { ... })
// Второй параметр использует синтаксис функции, соответствующий стилю Composition API.
export const useFavoriteStore = defineStore('favorite', () => {
  // ====== состояние: определить состояние с помощью ref ======
  // Используйте массив для хранения ID статей, добавленных в избранное
  const favoriteIds = ref([])

  // Восстановить список избранного из localStorage
  const saved = localStorage.getItem('blog-favorites')
  if (saved) {
    favoriteIds.value = JSON.parse(saved)
  }

  // ====== геттеры: определить вычисляемые свойства с помощью computed ======
  const favoriteCount = computed(() => favoriteIds.value.length)

  // Определить, добавлена ли статья в избранное
  function isFavorite(articleId) {
    return favoriteIds.value.includes(articleId)
  }

  // ====== действия: определение методов изменения с помощью обычных функций ======
  // Переключить статус избранного (в избранном → не в избранном, не в избранном → в избранном)
  function toggleFavorite(articleId) {
    const index = favoriteIds.value.indexOf(articleId)
    if (index === -1) {
      favoriteIds.value.push(articleId)     // Добавить в избранное
    } else {
      favoriteIds.value.splice(index, 1)    // Удалить из избранного
    }
    // Синхронизировать с localStorage (постоянное хранилище)
    localStorage.setItem('blog-favorites', JSON.stringify(favoriteIds.value))
  }

  return { favoriteIds, favoriteCount, isFavorite, toggleFavorite }
})

Второй параметр defineStore можно записать двумя способами:Options Store(аналогично синтаксису объекта data/methods/computed в Vue 2) иSetup Store(синтаксис функции с использованием Composition API). Рекомендуется использовать Setup Store, который согласуется с<script setup>синтаксисом, используемым внутри компонентов, и требует наименьших затрат на изучение.


Использование хранилища в компонентах

Любой компонент можетuseFavoriteStore()получить экземпляр хранилища, а затем напрямую читать/записывать состояние и вызывать действия.

Добавление кнопки избранного в BlogCard

Пример

<!-- Путь к файлу: src/components/BlogCard.vue -->
<script setup>
import { useFavoriteStore } from '../stores/useFavoriteStore.js'

const props = defineProps({
  id: Number,
  title: String,
  summary: String,
  date: String,
  category: String
})

const favoriteStore = useFavoriteStore()

// Вызывается при нажатии кнопки избранного
function handleFavorite(e) {
e.preventDefault() // Предотвратить переход по RouterLink
  favoriteStore.toggleFavorite(props.id)
}
</script>

<template>
  <RouterLink :to="{ name: 'post', params: { id } }" class="card-link">
    <div class="card">
      <span class="tag">{{ category }}</span>
      <h3>{{ title }}</h3>
      <p>{{ summary }}</p>
      <div class="card-footer">
        <span class="date">{{ date }}</span>
        <!-- Кнопка избранного: залитая, когда в избранном, контурная, когда не в избранном -->
        <button class="fav-btn" @click="handleFavorite">
          {{ favoriteStore.isFavorite(id) ? '&#x2665;' : '♡' }}
        </button>
      </div>
    </div>
  </RouterLink>
</template>

<style scoped>
.card-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 10px;
}
.fav-btn {
  background: none;
  border: none;
  font-size: 20px;
  cursor: pointer;
  color: #e74c3c;
  padding: 4px 8px;
}
.fav-btn:hover {
  transform: scale(1.2);
}
</style>

Также добавьте кнопку избранного на странице подробностей

Пример

<!-- Путь к файлу: src/views/PostView.vue (фрагмент кода) -->
<script setup>
import { useFavoriteStore } from '../stores/useFavoriteStore.js'

const favoriteStore = useFavoriteStore()
// ... остальной код
</script>

<template>
  <article v-if="article">
    <div class="post-header">
      <h1>{{ article.title }}</h1>
      <!-- Кнопка избранного на странице подробностей: состояние полностью синхронизировано с главной страницей -->
      <button class="fav-btn" @click="favoriteStore.toggleFavorite(article.id)">
        {{ favoriteStore.isFavorite(article.id) ? '&#x2665;В избранном' : '♡ В избранное' }}
      </button>
    </div>
    <!-- ... -->
  </article>
</template>

Состояние избранного на главной странице и странице подробной информации полностью синхронизировано: после добавления статьи в избранное на главной странице при переходе на страницу подробной информации автоматически отображается «В избранном».

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


Отображение количества избранного в NavBar

Отображайте счётчик избранного в правой части панели навигации; нажмите на него, чтобы увидеть список статей в избранном.

Пример

<!-- Путь к файлу: src/components/NavBar.vue -->
<script setup>
import { useFavoriteStore } from '../stores/useFavoriteStore.js'

const favoriteStore = useFavoriteStore()
</script>

<template>
  <header class="navbar">
    <a href="/" class="logo">ПРИМЕР Блог</a>
    <nav>
      <!-- Значок счётчика избранного -->
      <span class="fav-badge" v-if="favoriteStore.favoriteCount > 0">
Избранное {{ favoriteStore.favoriteCount }}
      </span>
    </nav>
  </header>
</template>

<style scoped>
.fav-badge {
  background: #e74c3c;
  color: #fff;
  padding: 4px 10px;
  border-radius: 12px;
  font-size: 13px;
}
</style>

Счётчик в NavBar автоматически синхронизируется с последним списком избранного — добавление статьи в избранное или удаление из него сразу изменяет число в панели навигации.


Обзор трёх основных возможностей Pinia

СпособностьСоответствующий кодПрименение в функции «Избранное»
Глобальное совместное использованиеЛюбой компонент может использовать useXxxStore()NavBar, BlogCard и PostView используют один и тот же список избранного.
РеактивностьПри изменении состояния все потребители автоматически обновляются.После добавления в избранное кнопка на главной странице становится красной, счётчик в панели навигации увеличивается на 1, а страница сведений обновляется синхронно.
ПерсистентностьЗапись в localStorage в actionsСписок избранного по-прежнему существует после обновления страницы.

Краткое содержание главы

В этой главе вы освоили полное использование Pinia для глобального управления состоянием: определение store с помощью defineStore (state/getters/actions), получение экземпляра с помощью useXxxStore в компонентах и реализацию сохранения данных через localStorage.

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

Другие расширения