Управление состоянием
Что такое управление состоянием?
Технически каждый экземпляр компонента Vue уже "управляет" своим реактивным состоянием. Возьмём для примера простой компонент счетчика:
vue
<script setup>
import { ref } from 'vue'
// состояние
const count = ref(0)
// действия
function increment() {
count.value++
}
</script>
<!-- представление -->
<template>{{ count }}</template>
Это самостоятельный блок, состоящий из следующих частей:
- Состояние - источник данных, который управляет нашим приложением;
- Представление - декларативное отображение состояния;
- Действия - возможные способы изменения состояния в ответ на пользовательский ввод из представления.
Это простое представление концепции "одностороннего потока данных":
Однако эта простота начинает нарушаться, когда у нас есть несколько компонентов, имеющих общее состояние:
- Несколько представлений могут зависеть от одного и того же фрагмента состояния.
- Действиям из разных представлений может потребоваться мутировать один и тот же фрагмент состояния.
В первом случае возможным обходным путем является "поднятие" общего состояния до общего компонента-предка, а затем передача его вниз в виде входных параметров. Однако это быстро становится утомительным в деревьях компонентов с глубокой иерархией, что приводит к другой проблеме, известной как пробрасывание входных параметров (prop drilling).
Во втором случае мы часто прибегаем к таким решениям, как обращение к прямым родительским и дочерним экземплярам через ссылки на элементы шаблона или попытка мутировать и синхронизировать несколько копий состояния через генерируемые события. Оба эти паттерна являются хрупкими и быстро приводят к сложно поддерживаемому коду.
Более простым и понятным решением является извлечение общего состояния из компонентов и управление им в глобальном синглтоне. Таким образом, наше дерево компонентов превращается в большое "представление", и любой компонент может получить доступ к состоянию или вызвать действия, независимо от того, где он находится в дереве!
Простое управление состояниям с помощью Reactivity API
Если у вас есть фрагмент состояния, который должен быть общим для нескольких экземпляров, вы можете использовать функцию reactive()
для создания реактивного объекта, а затем импортировать его в несколько компонентов:
js
// store.js
import { reactive } from 'vue'
export const store = reactive({
count: 0
})
vue
<!-- ComponentA.vue -->
<script setup>
import { store } from './store.js'
</script>
<template>Из компонента A: {{ store.count }}</template>
vue
<!-- ComponentB.vue -->
<script setup>
import { store } from './store.js'
</script>
<template>Из компонента B: {{ store.count }}</template>
Теперь при каждом изменении объекта store
и <ComponentA>
и <ComponentB>
будут автоматически обновлять свои представления - у нас теперь есть единый источник истины.
Однако это также означает, что любой компонент, импортирующий store
, может мутировать его по своему усмотрению:
template
<template>
<button @click="store.count++">
Из компонента B: {{ store.count }}
</button>
</template>
Хотя в простых случаях это работает, глобальное состояние, которое может произвольно изменяться любым компонентом, в долгосрочной перспективе будет не очень удобным в поддержке. Для того чтобы логика изменения состояния была централизованной, как и само состояние, рекомендуется определять методы хранилища с именами, выражающими замысел действий:
js
// store.js
import { reactive } from 'vue'
export const store = reactive({
count: 0,
increment() {
this.count++
}
})
template
<template>
<button @click="store.increment()">
Из компонента B: {{ store.count }}
</button>
</template>
Совет
Обратите внимание, что в обработчике клика используется store.increment()
со скобками — это необходимо для вызова метода с правильным контекстом, поскольку this
не метод компонента.
Хотя здесь мы используем в качестве хранилища один реактивный объект, вы также можете обмениваться реактивным состоянием, созданным с помощью других API реактивности, таких как ref()
или computed()
, или даже возвращать глобальное состояние из Composable:
js
import { ref } from 'vue'
// глобальное состояние, создаваемое в области видимости модуля
const globalCount = ref(1)
export function useCount() {
// локальное состояние, создаваемое для каждого компонента
const localCount = ref(1)
return {
globalCount,
localCount
}
}
Тот факт, что система реактивности Vue отделена от модели компонентов, делает ее чрезвычайно гибкой.
Соображения относительно SSR
Если вы создаете приложение, использующее отрисовку на стороне сервера (SSR), то описанная выше схема может привести к проблемам, поскольку хранилище является синглтоном, разделяемым на несколько запросов. Более подробно этот вопрос рассматривается в руководстве по SSR.
Pinia
Если в простых сценариях достаточно нашего решения по управлению состоянием, то в крупномасштабных производственных приложениях необходимо учитывать гораздо больше моментов:
- Более строгие соглашения для совместной работы команды
- Интеграция с инструментами Vue DevTools, включая временную шкалу, внутрикомпонентную проверку и отладку с перемещением во времени
- Механизм позволяющий модулям в приложении обновляться без перезагрузки страницы (Hot Module Replacement)
- Поддержка рендеринга на стороне сервера
Pinia - это библиотека управления состояниями, реализующая всё вышеперечисленное. Она поддерживается основной командой Vue и работает как с Vue 2, так и с Vue 3.
Некоторые пользователи могут быть знакомы с Vuex, предыдущей официальной библиотекой управления состояниями для Vue. Поскольку Pinia играет ту же роль в экосистеме, Vuex перешла в режим поддержки. Она продолжает работать, но больше не будет получать новых функций. Для новых приложений рекомендуется использовать Pinia.
Pinia начиналась как исследование того, как может выглядеть следующая итерация Vuex, и включала в себя множество идей из обсуждений основной команды Vuex 5. В конце концов, мы поняли, что Pinia уже реализует большую часть того, что мы хотели видеть во Vuex 5, и решили сделать ее новой рекомендацией.
По сравнению с Vuex, Pinia обеспечивает более простой API с меньшим количеством формальностей, предлагает API в стиле Composition-API и, что особенно важно, имеет надёжную поддержку вывода типов при использовании TypeScript.