Управление состоянием в приложениях на Vue

статья

Описание основных способов управления состоянием Vue приложения

Задача

Необходимо хранить общее состояние приложения с возможностью доступа к нему и возможностью вносить изменения в это состояние из любого компонента.

Решение

Есть несколько способов реализации общего состояния.

1. Хранение состояния в родительском компоненте

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

App.vue

<template>
  <AppChild :doubled-count="doubledCount" @increment="increment" />
  <AppChild :doubled-count="doubledCount" @increment="increment" />
</template>

<script setup>
  import { ref, computed } from 'vue'

  const count = ref(0)
  const doubledCount = computed(() => count.value * 2)
  const increment = () => {
    count.value += 1
  }
</script>

AppChild.vue

<template>
  <div>
    doubledCount: {{ doubledCount }}
  </div>
  <div>
    <button @click="$emit('increment')">increment</button>
  </div>
</template>

<script setup lang="ts">
  defineProps<{ doubledCount: number }>()
  defineEmits<{ increment: [number] }>()
</script>

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

2. Использование provide/inject

Здесь родительский компонент продолжает хранить общее состояние, но вместо передачи этого состояния через свойства компонента оно предоставляется посредством функции provide. Любой компонент ниже по дереву может получить доступ к этому состоянию с помощью функции inject. Изменения вносятся в общее состояние с помощью методов, предоставляемых через inject.

App.vue

<template>
  <AppChild />
  <AppChild />
</template>

<script setup>
  import { ref, provide, computed } from 'vue'

  const count = ref(0)
  const doubledCount = computed(() => count.value * 2)
  const increment = () => {
    count.value += 1
  }

  provide('appStore', { doubledCount, increment })
</script>

AppChild.vue

<template>
  <div>
    <AppDeepDescendant />
  </div>
</template>

AppDeepDescendant.vue

<template>
  <div>
    doubledCount: {{ appStore.doubledCount }}
  </div>
  <div>
    <button @click="appStore.increment">increment</button>
  </div>
</template>

<script setup lang="ts">
  import { inject } from 'vue'

  const appStore = inject('appStore')
</script>

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

App.vue

<template>
  <StoreProvider>
    <AppChild />
  </StoreProvider>

  <StoreProvider>
    <AppChild />
  </StoreProvider>
</template>

StoreProvider.vue

<script setup>
  import { ref, provide, computed } from 'vue'

  const count = ref(0)
  const doubledCount = computed(() => count.value * 2)
  const increment = () => {
    count.value += 1
  }

  provide('appStore', { doubledCount, increment })
</script>

В случае необходимости иметь одно единственное общее состояние хорошо подойдет третий способ.

3. Реализация глобального состояния в виде синглтон-композабл

Такую возможность предоставляет функция createGlobalState из библиотеки VueUse. Эта функция создает композабл, который используется всеми заинтересованными компонентами как обычный композабл. Этот композабл создает состояние только один раз, и все последующие его вызовы возвращают один и тот же экземпляр состояния.

App.vue

<template>
  <AppChild />
  <AppChild />
</template>

useAppStore.ts

import { createGlobalState } from '@vueuse/core'

export const useAppStore = createGlobalStore(() => {
  const count = ref(0)
  const doubledCount = computed(() => count.value * 2)
  const increment = () => {
    count.value += 1
  }

  return {
    doubledCount,
    increment
  }
})

AppDeepDescendant.vue

<template>
  <div>
    doubledCount: {{ appStore.doubledCount }}
  </div>
  <div>
    <button @click="appStore.increment">increment</button>
  </div>
</template>

<script setup>
  import { useAppStore } from './useAppStore'

  const appStore = useAppStore()
</script>

Иногда похожее поведение реализуется путём вынесения реактивных объектов в область видимости модуля (т.е. с внешней стороны композабл-функции). Такой способ имеет ряд недостатков (например, побочные эффекты при импорте модулей). Поэтому такому способу лучше предпочесть createGlobalState.

4. Гибридный подход с createInjectionState

Гибридом глобального композабл и provide/inject является подход, реализуемый с помощью функции createInjectionState из уже упомянутой выше библиотеки VueUse. Эта функция позволяет передавать состояние вниз по дереву компонентов, но не требует явного вызова inject в потребителях, а позволяет использовать композабл функции.

App.vue

<template>
  <StoreProvider>
    <AppChild />
  </StoreProvider>

  <StoreProvider>
    <AppChild />
  </StoreProvider>
</template>

StoreProvider.vue

<script setup>
  import { provideAppStore } from './useAppStore'

  provideAppStore()
</script>

useAppStore.ts

import { createInjectionState } from '@vueuse/core'

export const [provideAppStore, useAppStore] = createInjectionState(() => {
  const count = ref(0)
  const doubledCount = computed(() => count.value * 2)
  const increment = () => {
    count.value += 1
  }

  return {
    doubledCount,
    increment
  }
})

AppDeepDescendant.vue

<template>
  <div>
    doubledCount: {{ appStore.doubledCount }}
  </div>
  <div>
    <button @click="appStore.increment">increment</button>
  </div>
</template>

<script setup>
  import { useAppStore } from './useAppStore'

  const appStore = useAppStore()
</script>

Такой подход позволяет совместить преимущества чистого provide/inject (возможность иметь несколько корневых состояний одного типа) с удобством композабл функций (нет необходимости явного вызова inject с соответствующим ключом, что делает использование проще).

Заключение

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