DEV Community

Ajinkya Borade
Ajinkya Borade

Posted on

Understanding Vue Reactivity with Pinia Stores

At my workplace I was being tasked with creating a mock chat store for internal local dev work, and while doing so I made few notes about Vue (I had some experience, but not with hooks), So this is just my obsidian notes, I hope its useful to you :)

Table of Contents

  1. Ref and Reactive References
  2. Watch and Reactivity
  3. Pinia Store Integration
  4. Practical Examples
  5. Best Practices
  6. Common Gotchas

Ref and Reactive References

What is Ref?

ref is Vue's way of making primitive values reactive. It wraps the value in a reactive object with a .value property.

import { ref } from 'vue'

// Inside Pinia Store
export const useMyStore = defineStore('my-store', () => {
  // Creates a reactive reference
  const count = ref<number>(0)

  // To access or modify:
  function increment() {
    count.value++  // Need .value for refs
  }

  return {
    count,  // When exposed, components can use it without .value
    increment
  }
})
Enter fullscreen mode Exit fullscreen mode

Types of Refs in Stores

// Simple ref
const isLoading = ref<boolean>(false)

// Array ref
const messages = ref<Message[]>([])

// Complex object ref
const currentUser = ref<User | null>(null)

// Ref with undefined
const selectedId = ref<string | undefined>(undefined)
Enter fullscreen mode Exit fullscreen mode

Watch and Reactivity

Basic Watch Usage

import { watch, ref } from 'vue'

export const useMyStore = defineStore('my-store', () => {
  const messages = ref<Message[]>([])

  // Simple watch
  watch(messages, (newMessages, oldMessages) => {
    console.log('Messages changed:', newMessages)
  })
})
Enter fullscreen mode Exit fullscreen mode

Watch Options

// Immediate execution
watch(messages, (newMessages) => {
  // This runs immediately and on changes
}, { immediate: true })

// Deep watching
watch(messages, (newMessages) => {
  // Detects deep object changes
}, { deep: true })

// Multiple sources
watch(
  [messages, selectedId], 
  ([newMessages, newId], [oldMessages, oldId]) => {
    // Triggers when either changes
  }
)
Enter fullscreen mode Exit fullscreen mode

Pinia Store Integration

Store Structure with Refs

export const useMyStore = defineStore('my-store', () => {
  // State
  const items = ref<Item[]>([])
  const isLoading = ref(false)
  const error = ref<Error | null>(null)

  // Computed
  const itemCount = computed(() => items.value.length)

  // Actions
  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
  }
})
Enter fullscreen mode Exit fullscreen mode

Composing Stores

export const useMainStore = defineStore('main-store', () => {
  // Using another store
  const otherStore = useOtherStore()

  // Watching other store's state
  watch(
    () => otherStore.someState,
    (newValue) => {
      // React to other store's changes
    }
  )
})
Enter fullscreen mode Exit fullscreen mode

Practical Examples

Auto-refresh Implementation

export const useChatStore = defineStore('chat-store', () => {
  const messages = ref<Message[]>([])
  const refreshInterval = ref<number | null>(null)
  const isRefreshing = ref(false)

  // Watch for auto-refresh state
  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
  }
})
Enter fullscreen mode Exit fullscreen mode

Loading State Management

export const useDataStore = defineStore('data-store', () => {
  const data = ref<Data[]>([])
  const isLoading = ref(false)
  const error = ref<Error | null>(null)

  // Watch loading state for side effects
  watch(isLoading, (loading) => {
    if (loading) {
      // Show loading indicator
    } else {
      // Hide loading indicator
    }
  })

  // Watch for errors
  watch(error, (newError) => {
    if (newError) {
      // Handle error (show notification, etc.)
    }
  })
})
Enter fullscreen mode Exit fullscreen mode

Best Practices

1. Ref Initialisation

// ❌ Bad
const data = ref()  // Type is 'any'

// ✅ Good
const data = ref<string[]>([])  // Explicitly typed
Enter fullscreen mode Exit fullscreen mode

2. Watch Cleanup

// ❌ Bad - No cleanup
watch(source, () => {
  const timer = setInterval(() => {}, 1000)
})

// ✅ Good - With cleanup
watch(source, () => {
  const timer = setInterval(() => {}, 1000)
  return () => clearInterval(timer)  // Cleanup function
})
Enter fullscreen mode Exit fullscreen mode

3. Computed vs Watch

// ❌ Bad - Using watch for derived state
watch(items, (newItems) => {
  itemCount.value = newItems.length
})

// ✅ Good - Using computed for derived state
const itemCount = computed(() => items.value.length)
Enter fullscreen mode Exit fullscreen mode

4. Store Organization

// ✅ Good store organization
export const useStore = defineStore('store', () => {
  // State refs
  const data = ref<Data[]>([])
  const isLoading = ref(false)

  // Computed properties
  const isEmpty = computed(() => data.value.length === 0)

  // Watchers
  watch(data, () => {
    // Handle data changes
  })

  // Actions
  const fetchData = async () => {
    // Implementation
  }

  // Return public interface
  return {
    data,
    isLoading,
    isEmpty,
    fetchData
  }
})
Enter fullscreen mode Exit fullscreen mode

Common Gotchas

  1. Forgetting .value
// ❌ Bad
const count = ref(0)
count++ // Won't work

// ✅ Good
count.value++
Enter fullscreen mode Exit fullscreen mode
  1. Watch Timing
// ❌ Bad - Might miss initial state
watch(source, () => {})

// ✅ Good - Catches initial state
watch(source, () => {}, { immediate: true })
Enter fullscreen mode Exit fullscreen mode
  1. Memory Leaks
// ❌ Bad - No cleanup
const store = useStore()
setInterval(() => {
  store.refresh()
}, 1000)

// ✅ Good - With cleanup
const intervalId = setInterval(() => {
  store.refresh()
}, 1000)
onBeforeUnmount(() => clearInterval(intervalId))
Enter fullscreen mode Exit fullscreen mode

Remember: Always consider cleanup, type safety, and proper organization when working with refs and watches in Pinia stores

Top comments (0)