Потужна реактивність Vue 3.0 та Pinia Stores

Цей шаблон був створений для внутрішніх потреб розробки, щоб мати можливість тестувати чат у локальному середовищі, а під час роботи над ним я зробив кілька нотаток про Vue (я вже мав досвід роботи з ним, але без використання хуків). Тож це просто мої нотатки в Obsidian, сподіваюся, вони будуть корисні 🙂

pic

Повний посібник із освоєння Composition API Vue, реактивних патернів і інтеграції Pinia store. Ідеально підходить для розробників, які хочуть підняти свої навички в Vue.js на новий рівень.

## Зміст

  1. Ref і реактивні посилання
  2. Watch і реактивність
  3. Composables
  4. Реактивність Vue API (reactive vs ref)
  5. Інтеграція Pinia Store
  6. Практичні приклади
  7. Кращі практики
  8. Типові проблеми
  9. Розширені патерни
  10. Оптимізація продуктивності

## Ref і реактивні посилання

Розуміння основ системи реактивності Vue є ключовим для створення надійних додатків. У цьому розділі розглядається, як управляти реактивним станом за допомогою refs і реактивних посилань, з практичними прикладами та інтеграцією TypeScript.

Що таке Ref?

ref — це спосіб Vue зробити примітивні значення реактивними. Він обгортає значення в реактивний об'єкт з властивістю .value.

import { ref } from ‘vue’

// У Pinia Store  
export const useMyStore = defineStore(‘my-store’, () => {  
 // Створює реактивне посилання  
 const count = ref(0)  

 // Для доступу або зміни:  
 function increment() {  
 count.value++ // Необхідно використовувати .value для refs  
 }

return {  
 count, // Коли передається в компоненти, вони можуть використовувати його без .value  
 increment  
 }  
})  

Типи Ref в Stores

// Простий ref  
const isLoading = ref(false)

// Масив ref  
const messages = ref([])

// Складний об'єкт ref  
const currentUser = ref(null)

// Ref з undefined  
const selectedId = ref(undefined)  

## Watch і реактивність

Освоєння можливостей Vue для спостереження дозволяє ефективно реагувати на зміни стану і створювати динамічні, адаптивні додатки. Дізнайтесь, як ефективно використовувати watchers і працювати з складними реактивними патернами.

Основне використання Watch

import { watch, ref } from ‘vue’

export const useMyStore = defineStore(‘my-store’, () => {  
 const messages = ref([])  

 // Простий watch  
 watch(messages, (newMessages, oldMessages) => {  
 console.log(‘Повідомлення змінено:’, newMessages)  
 })  
})  

Опції Watch

// Негайне виконання  
watch(messages, (newMessages) => {  
 // Це виконується негайно та при змінах  
}, { immediate: true })

// Глибоке спостереження  
watch(messages, (newMessages) => {  
 // Виявляє глибокі зміни об'єкта  
}, { deep: true })

// Багато джерел  
watch(  
 [messages, selectedId],   
 ([newMessages, newId], [oldMessages, oldId]) => {  
 // Тригерить при зміні будь-якого з них  
 }  
)  

## Composables

Composables — це основа для створення повторно використовуваної логіки у Vue 3. Цей розділ покаже вам, як створювати потужну, багаторазову функціональність, яку можна поділити по всьому додатку, при цьому зберігаючи код чистим і організованим.

Створення кастомних Composables

Composables — це повторно використовувані функції з реактивним станом, які слідують конвенції з префіксом use.

// useCounter.ts  
import { ref, computed } from ‘vue’

export function useCounter(initialValue = 0) {  
 const count = ref(initialValue)  
 const doubleCount = computed(() => count.value * 2)

function increment() {  
 count.value++  
 }

function decrement() {  
 count.value —   
 }

return {  
 count,  
 doubleCount,  
 increment,  
 decrement  
 }  
}  

Інтеграція в життєвий цикл Composable

// useMousePosition.ts  
import { ref, onMounted, onUnmounted } from ‘vue’

export function useMousePosition() {  
 const x = ref(0)  
 const y = ref(0)

function update(event: MouseEvent) {  
 x.value = event.pageX  
 y.value = event.pageY  
 }

onMounted(() => {  
 window.addEventListener(‘mousemove’, update)  
 })

onUnmounted(() => {  
 window.removeEventListener(‘mousemove’, update)  
 })

return { x, y }  
}  

Асинхронні Composables
```ts
// useAsyncData.ts
import { ref, watchEffect } from ‘vue’

export function useAsyncData(asyncGetter: () => Promise) {
const data = ref(null)
const error = ref(null)
const isLoading = ref(false)

async function fetch() {
isLoading.value = true
error.value = null

try {
data.value = await asyncGetter()
} catch (e) {
error.value = e as Error
} finally {
isLoading.value = false
}
}

watchEffect(() => {
fetch()
})

return {
data,
error,
isLoading,
refresh: fetch
}
}
```

Залежності Composables

// useUserProfile.ts  
import { computed } from ‘vue’  
import { useAuth } from ‘./useAuth’  
import { useApi } from ‘./useApi’

export function useUserProfile() {  
 const { user } = useAuth()  
 const { get } = useApi()  

 const userProfile = computed(() => {  
 if (!user.value) return null  
 return get(`/users/${user.value.id}/profile`)  
 })

return {  
 userProfile  
 }  
}  

Повторно використовувана валідація форм
```ts
// useFormValidation.ts
import { ref, computed } from ‘vue’

export function useFormValidation>(initialState: T) {
const formData = ref(initialState)
const errors = ref>>({})

const isValid = computed(() => Object.keys(errors.value).length === 0)

function validate(rules: Record string | null>) {
errors.value = {}

Object.entries(rules).forEach(([field, validator]) => {
const error = validator(formData.value[field])
if (error) {
errors.value[field as keyof T] = error
}
})

return isValid.value
}

return {
formData,
errors,
isValid,
validate
}
}
```

Використання Composables в компонентах





## Реактивність Vue API

Глибоке занурення в потужну систему реактивності Vue.
Learn how to leverage reactive objects, refs, and computed properties to build dynamic and efficient applications.

Використання реактивних об'єктів
Метод reactive створює реактивний проксі для об'єкта, роблячи всі його властивості глибоко реактивними.

// Основний реактивний об'єкт  
import { reactive } from ‘vue’

interface User {  
 name: string  
 age: number  
 settings: {  
 theme: string  
 notifications: boolean  
 }  
}

const user = reactive({  
 name: ‘John’,  
 age: 30,  
 settings: {  
 theme: ‘dark’,  
 notifications: true  
 }  
})

// Прямий доступ до властивості (без .value)  
console.log(user.name)  
user.age = 31  

Reactivity vs Ref
```ts
// Порівняння використання reactive та ref
import { reactive, ref } from ‘vue’

// Використання ref
const count = ref(0)
const user = ref({
name: ‘John’,
age: 30
})

// Потрібно .value для ref
count.value++
user.value.age++

// Використання reactive
const state = reactive({
count: 0,
user: {
name: ‘John’,
age: 30
}
})

// Прямий доступ до властивостей
state.count++
state.user.age++
```

Обмеження та робота з типами

// ❌ Обмеження reactive  
import { reactive } from ‘vue’

// Не деструктуруйте реактивні об'єкти  
const state = reactive({ count: 0 })  
const { count } = state // Втрачається реактивність!

// ✅ Замість цього використовуйте computed або методи  
import { reactive, computed } from ‘vue’

const state = reactive({ count: 0 })  
const doubleCount = computed(() => state.count * 2)

// Або зберігайте посилання на вкладені об'єкти  
const nested = reactive({  
 user: {  
 profile: {  
 name: ‘John’  
 }  
 }  
})

// Це зберігає реактивність  
const profile = nested.user.profile  

Реактивні масиви та колекції
```ts
import { reactive } from ‘vue’

interface TodoItem {
id: number
text: string
completed: boolean
}

const todos = reactive([])

// Методи зберігають реактивність
function addTodo(text: string) {
todos.push({
id: Date.now(),
text,
completed: false
})
}

// Робота з реактивними колекціями
const collection = reactive(new Map())
collection.set(‘key’, 1)
```

Комбінування з Composables
```ts
// useTaskManager.ts
import { reactive, computed } from ‘vue’

interface Task {
id: number
title: string
completed: boolean
}

export function useTaskManager() {
const state = reactive({
tasks: [] as Task[],
filter: ‘all’ as ‘all’ | ‘active’ | ‘completed’
})

const filteredTasks = computed(() => {
switch (state.filter) {
case ‘active’:
return state.tasks.filter(task => !task.completed)
case ‘completed’:
return state.tasks.filter(task => task.completed)
default:
return state.tasks
}
})

function addTask(title: string) {
state.tasks.push({
id: Date.now(),
title,
completed: false
})
}

function toggleTask(id: number) {
const task = state.tasks.find(t => t.id === id)
if (task) {
task.completed = !task.completed
}
}

return {
state,
filteredTasks,
addTask,
toggleTask
}
}
```

Кращі практики роботи з реактивністю
```ts
// ✅ Кращі практики
import { reactive, toRefs } from ‘vue’

// 1. Використовуйте інтерфейси для безпеки типів
interface State {
loading: boolean
error: Error | null
data: string[]
}

// 2. Ініціалізуйте всі властивості
const state = reactive({
loading: false,
error: null,
data: []
})

// 3. Використовуйте toRefs, коли потрібно деструктурувати
function useFeature() {
const state = reactive({
foo: 1,
bar: 2
})

// Зробіть це безпечним для деструктурування
return toRefs(state)
}

// 4.
Avoid nested reactivity when possible
// ❌ Погано
const nested = reactive({
user: reactive({
profile: reactive({
name: ‘John’
})
})
})

// ✅ Добре
const state = reactive({
user: {
profile: {
name: ‘John’
}
}
})


**Інтеграція з TypeScript**  
```ts  
// Розширене використання TypeScript з reactive  
import { reactive } from ‘vue’

// Визначення складних типів  
interface User {  
 id: number  
 name: string  
 preferences: {  
 theme: ‘light’ | ‘dark’  
 notifications: boolean  
 }  
}

interface AppState {  
 currentUser: User | null  
 isAuthenticated: boolean  
 settings: Map  
}

// Створення реактивного стану з типами  
const state = reactive({  
 currentUser: null,  
 isAuthenticated: false,  
 settings: new Map()  
})

// Типізовані методи  
function updateUser(user: Partial<User>) {  
 if (state.currentUser) {  
 Object.assign(state.currentUser, user)  
 }  
}

// Тільки для читання реактивний стан  
import { readonly } from ‘vue’  
const readonlyState = readonly(state)  

## Інтеграція з Pinia Store

Дізнайтеся, як ефективно інтегрувати Pinia stores з Composition API Vue. Ознайомтесь з найкращими практиками управління станом та як структурувати свої stores для масштабованості.

Структура Store з Refs
```ts
export const useMyStore = defineStore(‘my-store’, () => {
// Стан
const items = ref([])
const isLoading = ref(false)
const error = ref(null)

// Обчислене
const itemCount = computed(() => items.value.length)

// Дії
const fetchItems = async () => {
isLoading.value = true
try {
items.value = await api.getItems()
} catch (e) {
error.value = e as Error
} finally {
isLoading.value = false
}
}

return {
items,
isLoading,
error,
itemCount,
fetchItems
}
})
```

Комбінування Stores
```ts
export const useMainStore = defineStore(‘main-store’, () => {
// Використання іншого store
const otherStore = useOtherStore()

// Слідкування за станом іншого store
watch(
() => otherStore.someState,
(newValue) => {
// Реакція на зміни в іншому store
}
)
})
```

## Практичні приклади

Приклади з реального життя, які демонструють, як реалізувати поширені функції та патерни у Vue-додатках. Ці приклади покажуть вам, як застосувати теорію на практиці.

Реалізація автоновлення
```ts
export const useChatStore = defineStore(‘chat-store’, () => {
const messages = ref([])
const refreshInterval = ref(null)
const isRefreshing = ref(false)

// Спостереження за станом автоновлення
watch(isRefreshing, (shouldRefresh) => {
if (shouldRefresh) {
startAutoRefresh()
} else {
stopAutoRefresh()
}
})

const startAutoRefresh = () => {
refreshInterval.value = window.setInterval(() => {
fetchNewMessages()
}, 5000)
}

const stopAutoRefresh = () => {
if (refreshInterval.value) {
clearInterval(refreshInterval.value)
refreshInterval.value = null
}
}

return {
messages,
isRefreshing,
startAutoRefresh,
stopAutoRefresh
}
})
```

Управління станом завантаження

export const useDataStore = defineStore(‘data-store’, () => {  
 const data = ref([])  
 const isLoading = ref(false)  
 const error = ref(null)

// Спостереження за станом завантаження для побічних ефектів  
 watch(isLoading, (loading) => {  
 if (loading) {  
 // Показати індикатор завантаження  
 } else {  
 // Сховати індикатор завантаження  
 }  
 })

// Спостереження за помилками  
 watch(error, (newError) => {  
 if (newError) {  
 // Обробка помилки (показ повідомлення, тощо)  
 }  
 })  
})  

## Кращі практики

Уникайте поширених помилок і головного болю при налагодженні, ознайомившись з цими часто зустрічаються проблемами та їх рішеннями.

1. Ініціалізація Ref

// ❌ Погано  
const data = ref() // Тип ‘any’

// ✅ Добре  
const data = ref([]) // Явно вказаний тип  

2.
Watch Cleanup

// ❌ Погано — Без очищення  
watch(source, () => {  
 const timer = setInterval(() => {}, 1000)  
})

// ✅ Добре — З очищенням  
watch(source, () => {  
 const timer = setInterval(() => {}, 1000)  
 return () => clearInterval(timer) // Функція очищення  
})  

3. Computed vs Watch

// ❌ Погано — Використання watch для похідного стану  
watch(items, (newItems) => {  
 itemCount.value = newItems.length  
})

// ✅ Добре — Використання computed для похідного стану  
const itemCount = computed(() => items.value.length)  

4. Організація Store

// ✅ Добра організація store  
export const useStore = defineStore(‘store’, () => {  
 // Стан refs  
 const data = ref([])  
 const isLoading = ref(false)

// Обчислені властивості  
 const isEmpty = computed(() => data.value.length === 0)

// Спостерігачі  
 watch(data, () => {  
 // Обробка змін даних  
 })

// Дії  
 const fetchData = async () => {  
 // Реалізація  
 }

// Публічний інтерфейс  
 return {  
 data,  
 isLoading,  
 isEmpty,  
 fetchData  
 }  
})  

## Загальні проблеми

  1. Забування .value
// ❌ Погано  
const count = ref(0)  
count++ // Не працюватиме

// ✅ Добре  
count.value++  
  1. Час спостереження (Watch Timing)
// ❌ Погано — Може пропустити початковий стан  
watch(source, () => {})

// ✅ Добре — Захоплює початковий стан  
watch(source, () => {}, { immediate: true })  
  1. Витоки пам'яті (Memory Leaks)
// ❌ Погано — Без очищення  
const store = useStore()  
setInterval(() => {  
 store.refresh()  
}, 1000)

// ✅ Добре — З очищенням  
const intervalId = setInterval(() => {  
 store.refresh()  
}, 1000)  
onBeforeUnmount(() => clearInterval(intervalId))  

## Складні патерни

Підніміть свої навички Vue.js на новий рівень за допомогою складних патернів та технік для створення комплексних і масштабованих додатків.

Складна комунікація між компонентами
```ts
// Приклад складних патернів комунікації між компонентами
export function useComponentBridge() {
const events = ref(new Map())

function emit(event: string, data: any) {
if (events.value.has(event)) {
events.value.get(event).forEach((handler: Function) => handler(data))
}
}

function on(event: string, handler: Function) {
if (!events.value.has(event)) {
events.value.set(event, new Set())
}
events.value.get(event).add(handler)

return () => events.value.get(event).delete(handler)
}

return { emit, on }
}
```

Стратегії тестування

Дізнайтесь, як ефективно тестувати ваші компоненти Vue та stores, використовуючи сучасні практики та інструменти тестування.

Тестування Composables
```ts
import { renderComposable } from ‘@testing-library/vue-composables’
import { useCounter } from ‘./useCounter’

describe(‘useCounter’, () => {
test(‘should increment counter’, () => {
const { result } = renderComposable(() => useCounter())

expect(result.current.count.value).toBe(0)
result.current.increment()
expect(result.current.count.value).toBe(1)
})
})
```

## Оптимізація продуктивності

Техніки та стратегії для оптимізації ваших Vue-додатків для кращої продуктивності та користувацького досвіду.

Оптимізація обчислених властивостей (Computed Property Optimization)
```ts
// Оптимізація обчислених властивостей для кращої продуктивності
const expensiveComputation = computed(() => {
return memoize(() => {
// Дорога обчислювальна операція
return result
})
})

Перекладено з: Supercharged Vue 3.0 Reactivity + Pinia Stores

Leave a Reply

Your email address will not be published. Required fields are marked *