All skills
vuejs-ai avatar

/vue-pinia-best-practices

@bc922e4 official
by Vue AIvuejs-ai/skills2.9k stars
167

Pinia stores, state management patterns, store setup, and reactivity with stores.

Use this Skill: https://skilld.dev/gh/vuejs-ai/skills/vue-pinia-best-practices

This session only. Nothing lands on disk.

referencestate-url-for-ephemeral-filters.md

≈1.8k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Use URL State for Shareable Filters Instead of Stores

Impact: MEDIUM - Storing ephemeral UI state like filters, search queries, pagination, and sorting only in Pinia or component state means users lose that state on page refresh and cannot share links to specific filtered views. This hurts user experience and SEO.

For state that represents a "view" of data, use URL query parameters instead of or alongside stores.

Task Checklist

  • Identify ephemeral UI state: filters, search, sort, pagination, tab selection
  • Store such state in URL query parameters
  • Sync URL state with component/store state bidirectionally
  • Consider using VueUse's useRouteQuery for type-safe URL state
  • Keep URL state minimal and human-readable

The Problem: Store-Only State

<script setup>
import { ref } from 'vue'
import { useProductStore } from '@/stores/products'

const productStore = useProductStore()

// Filter state in component/store only
const selectedCategory = ref('all')
const priceRange = ref([0, 1000])
const sortBy = ref('newest')
const searchQuery = ref('')

// Problems with this approach:
// 1. User refreshes page → filters reset to defaults
// 2. User bookmarks page → bookmark doesn't include filter state
// 3. User shares link → friend sees unfiltered view
// 4. Back button doesn't restore previous filter state
</script>

The Solution: URL-Based State

<script setup>
import { computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()

// Read filter state FROM URL
const selectedCategory = computed({
  get: () => route.query.category || 'all',
  set: (value) => updateQuery({ category: value === 'all' ? undefined : value })
})

const sortBy = computed({
  get: () => route.query.sort || 'newest',
  set: (value) => updateQuery({ sort: value === 'newest' ? undefined : value })
})

const searchQuery = computed({
  get: () => route.query.q || '',
  set: (value) => updateQuery({ q: value || undefined })
})

const page = computed({
  get: () => parseInt(route.query.page) || 1,
  set: (value) => updateQuery({ page: value === 1 ? undefined : value })
})

// Helper to update URL without full navigation
function updateQuery(newParams) {
  router.push({
    query: {
      ...route.query,
      ...newParams
    }
  })
}
</script>

<template>
  <div>
    <input v-model="searchQuery" placeholder="Search...">

    <select v-model="selectedCategory">
      <option value="all">All Categories</option>
      <option value="electronics">Electronics</option>
      <option value="clothing">Clothing</option>
    </select>

    <select v-model="sortBy">
      <option value="newest">Newest</option>
      <option value="price-low">Price: Low to High</option>
      <option value="price-high">Price: High to Low</option>
    </select>

    <!-- URL now looks like: /products?category=electronics&sort=price-low&q=phone -->
    <!-- Users can bookmark, share, and refresh without losing state -->
  </div>
</template>

Using VueUse for Cleaner Code

VueUse provides useRouteQuery for type-safe URL state:

<script setup>
import { useRouteQuery } from '@vueuse/router'

// Automatically syncs with URL query parameters
const category = useRouteQuery('category', 'all')
const sort = useRouteQuery('sort', 'newest')
const search = useRouteQuery('q', '')
const page = useRouteQuery('page', 1, { transform: Number })
const showOutOfStock = useRouteQuery('inStock', false, { transform: Boolean })

// Arrays work too
const selectedTags = useRouteQuery('tags', [], {
  transform: (v) => Array.isArray(v) ? v : v ? [v] : []
})
</script>

<template>
  <input v-model="search" placeholder="Search...">
  <select v-model="category">...</select>
  <input type="checkbox" v-model="showOutOfStock"> Show out of stock
</template>

Hybrid Approach: URL + Store

For complex state, sync URL with store:

// stores/productFilters.js
import { defineStore } from 'pinia'
import { useRoute, useRouter } from 'vue-router'
import { watch } from 'vue'

export const useProductFiltersStore = defineStore('productFilters', () => {
  const route = useRoute()
  const router = useRouter()

  // Local reactive state
  const category = ref('all')
  const sortBy = ref('newest')
  const searchQuery = ref('')
  const page = ref(1)

  // Initialize from URL on store creation
  function initFromUrl() {
    category.value = route.query.category || 'all'
    sortBy.value = route.query.sort || 'newest'
    searchQuery.value = route.query.q || ''
    page.value = parseInt(route.query.page) || 1
  }

  // Sync state changes TO URL
  function syncToUrl() {
    router.replace({
      query: {
        category: category.value !== 'all' ? category.value : undefined,
        sort: sortBy.value !== 'newest' ? sortBy.value : undefined,
        q: searchQuery.value || undefined,
        page: page.value > 1 ? page.value : undefined
      }
    })
  }

  // Watch for URL changes (back/forward navigation)
  watch(() => route.query, initFromUrl, { immediate: true })

  // Watch for state changes and sync to URL
  watch([category, sortBy, searchQuery, page], syncToUrl)

  return {
    category,
    sortBy,
    searchQuery,
    page,
    initFromUrl
  }
})

What Goes in URL vs Store

State Type URL Store Notes
Filters Yes Optional Shareable, bookmarkable
Search query Yes Optional SEO benefit
Pagination Yes Optional Deep linking
Sort order Yes Optional User expectation
Selected tab Yes Optional Deep linking
Modal open state Maybe Yes Usually not shareable
Form draft No Yes Private, temporary
User session No Yes Security
Shopping cart No Yes Persistence needed

Benefits of URL State

  1. Shareable: Users can share exact filtered views
  2. Bookmarkable: Save specific searches/filters
  3. Browser history: Back/forward works as expected
  4. SEO: Search engines can index filtered pages
  5. Refresh-safe: State survives page reload
  6. Deep linking: Direct links to specific states

Clean URL Best Practices

// GOOD: Clean, readable URLs
/products?category=electronics&sort=price&q=phone

// AVOID: Overly complex URLs
/products?filters=%7B%22category%22%3A%22electronics%22%7D
// Use defaults to keep URLs minimal
const sort = useRouteQuery('sort', 'newest')

// URL shows: /products (when using default sort)
// URL shows: /products?sort=price-low (when changed)

Reference

Source: SKILL.md on GitHub

No alerts16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    This skill provides educational content and best practices for Pinia state management in Vue.js applications. It is a documentation-focused skill with no malicious patterns detected.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer7mo

    7 files scanned · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at bc922e4. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 8 months ago
version
1.0.0
author
github.com/vuejs-ai
  • Vue
  • pinia
  • state-management
  • reactivity
  • store-setup
  • devtools
  • ssr

README badge

README badge for vuejs-ai/skills/vue-pinia-best-practices

Provides Pinia store setup patterns, reactivity gotchas, and state management conventions for Vue apps. Covers destructuring pitfalls, DevTools integration, SSR considerations, and filter persistence strategies.

Generated from the current SKILL.md.

Does this skill cover setup stores and options stores?
Yes. The skill addresses both patterns, including common issues like setup stores missing state in DevTools or SSR, and provides guidance on when to use each.
What reactivity issues does this skill handle?
It covers store destructuring breaking reactivity, method binding losing context in templates, and how to maintain reactive updates when accessing store properties.
Does this skill address SSR or DevTools integration?
Yes. It includes troubleshooting for setup stores not exposing state correctly in DevTools and SSR environments.
Does this cover ephemeral state like filters or URL sync?
Yes. The skill includes patterns for handling filters that reset on refresh and guidance on syncing state with URLs for shareable application state.

Generated from the current SKILL.md. These answers refresh after source changes.