chore: init monorepo with existing website and plans
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
---
|
||||
title: Fix "No Active Pinia" Error - Store Setup Timing
|
||||
impact: HIGH
|
||||
impactDescription: Using Pinia stores before app.use(pinia) causes "getActivePinia was called but there was no active Pinia" error
|
||||
type: gotcha
|
||||
tags: [vue3, pinia, state-management, setup, initialization, error]
|
||||
---
|
||||
|
||||
# Fix "No Active Pinia" Error - Store Setup Timing
|
||||
|
||||
**Impact: HIGH** - The error "getActivePinia() was called but there was no active Pinia" is one of the most common Pinia errors. It occurs when you try to use a store before Pinia has been installed on the Vue app, causing your application to crash.
|
||||
|
||||
## Task Checklist
|
||||
|
||||
- [ ] Ensure `app.use(pinia)` is called before `app.mount()`
|
||||
- [ ] Ensure `app.use(pinia)` is called before `app.use(router)` if router guards use stores
|
||||
- [ ] Never call `useXxxStore()` in module-level (top-level) code
|
||||
- [ ] Only call `useXxxStore()` inside setup functions, composables, or after app initialization
|
||||
- [ ] Check for `<script setup>` vs `<script>` - the latter runs too early
|
||||
|
||||
## The Error
|
||||
|
||||
```
|
||||
[🍍]: "getActivePinia()" was called but there was no active Pinia.
|
||||
Did you forget to install pinia?
|
||||
```
|
||||
|
||||
## Common Cause 1: Wrong Plugin Order
|
||||
|
||||
```javascript
|
||||
// main.js - WRONG ORDER
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import router from './router' // Router uses a store in navigation guard
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// WRONG: Router is installed first, but its guards use stores
|
||||
app.use(router) // Router guard calls useAuthStore() - FAILS!
|
||||
app.use(createPinia())
|
||||
app.mount('#app')
|
||||
```
|
||||
|
||||
**Fix: Install Pinia first:**
|
||||
|
||||
```javascript
|
||||
// main.js - CORRECT ORDER
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import router from './router'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// CORRECT: Pinia installed before anything that uses stores
|
||||
app.use(createPinia())
|
||||
app.use(router) // Now router guards can safely use stores
|
||||
app.mount('#app')
|
||||
```
|
||||
|
||||
## Common Cause 2: Store Used at Module Level
|
||||
|
||||
```javascript
|
||||
// api.js - WRONG: Module-level store usage
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
// This runs immediately when the module is imported!
|
||||
const authStore = useAuthStore() // ERROR: No active Pinia yet
|
||||
|
||||
export function fetchUser() {
|
||||
return fetch('/api/user', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authStore.token}`
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**Fix: Call useStore inside functions:**
|
||||
|
||||
```javascript
|
||||
// api.js - CORRECT: Store used inside function
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
export function fetchUser() {
|
||||
// Store is accessed when function is called, not when module loads
|
||||
const authStore = useAuthStore()
|
||||
|
||||
return fetch('/api/user', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authStore.token}`
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Common Cause 3: Script Tag Missing "setup"
|
||||
|
||||
```vue
|
||||
<!-- WRONG: <script> runs before component setup -->
|
||||
<script>
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
// This runs too early, before the component is set up
|
||||
const userStore = useUserStore() // ERROR!
|
||||
|
||||
export default {
|
||||
// ...
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
**Fix: Use `<script setup>` or move to setup function:**
|
||||
|
||||
```vue
|
||||
<!-- CORRECT: <script setup> runs at the right time -->
|
||||
<script setup>
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
// This runs during component setup, Pinia is active
|
||||
const userStore = useUserStore() // Works!
|
||||
</script>
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- CORRECT: Options API with setup function -->
|
||||
<script>
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
// Called during component initialization
|
||||
const userStore = useUserStore() // Works!
|
||||
return { userStore }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Common Cause 4: mapStores with Parentheses
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import { mapStores } from 'pinia'
|
||||
import { useProductsStore } from '@/stores/products'
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
// WRONG: Called the function instead of passing it
|
||||
...mapStores(useProductsStore()) // ERROR!
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
**Fix: Pass the function reference, not the result:**
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import { mapStores } from 'pinia'
|
||||
import { useProductsStore } from '@/stores/products'
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
// CORRECT: Pass the function without calling it
|
||||
...mapStores(useProductsStore) // No parentheses!
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Common Cause 5: Router Guards Before Pinia
|
||||
|
||||
```javascript
|
||||
// router/index.js - WRONG
|
||||
import { createRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const router = createRouter({ /* ... */ })
|
||||
|
||||
// This guard is registered immediately
|
||||
router.beforeEach((to) => {
|
||||
// When this runs during app startup, Pinia might not be ready
|
||||
const authStore = useAuthStore() // May fail!
|
||||
|
||||
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
|
||||
return '/login'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Fix: Use lazy store access or ensure plugin order:**
|
||||
|
||||
```javascript
|
||||
// router/index.js - CORRECT
|
||||
import { createRouter } from 'vue-router'
|
||||
|
||||
const router = createRouter({ /* ... */ })
|
||||
|
||||
router.beforeEach((to) => {
|
||||
// Dynamically import to avoid module-level execution
|
||||
const { useAuthStore } = await import('@/stores/auth')
|
||||
const authStore = useAuthStore()
|
||||
|
||||
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
|
||||
return '/login'
|
||||
}
|
||||
})
|
||||
|
||||
// OR ensure main.js has correct order:
|
||||
// app.use(pinia)
|
||||
// app.use(router)
|
||||
```
|
||||
|
||||
## Debugging Checklist
|
||||
|
||||
When you see "No active Pinia":
|
||||
|
||||
1. **Check main.js order**: Is `app.use(pinia)` before other plugins?
|
||||
2. **Search for top-level useStore calls**: Any store usage outside functions/setup?
|
||||
3. **Check script tags**: Using `<script>` instead of `<script setup>`?
|
||||
4. **Check mapStores usage**: Using `useStore()` instead of `useStore`?
|
||||
5. **Check import chains**: Does an early import trigger store usage?
|
||||
|
||||
## Safe Pattern: Conditional Store Access
|
||||
|
||||
```javascript
|
||||
// For code that might run before Pinia is ready
|
||||
import { getActivePinia } from 'pinia'
|
||||
|
||||
export function safelyUseStore() {
|
||||
const pinia = getActivePinia()
|
||||
|
||||
if (!pinia) {
|
||||
console.warn('Pinia not initialized yet')
|
||||
return null
|
||||
}
|
||||
|
||||
const { useUserStore } = await import('@/stores/user')
|
||||
return useUserStore()
|
||||
}
|
||||
```
|
||||
|
||||
## Reference
|
||||
- [Vue Land FAQ - No Active Pinia](https://vue-land.github.io/faq/no-active-pinia)
|
||||
- [Pinia - Using a Store Outside of a Component](https://pinia.vuejs.org/core-concepts/outside-component-usage.html)
|
||||
- [Pinia - Getting Started](https://pinia.vuejs.org/getting-started.html)
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
---
|
||||
title: Return All State Properties in Pinia Setup Stores
|
||||
impact: HIGH
|
||||
impactDescription: Not returning state properties in setup stores breaks SSR, DevTools, and plugin compatibility
|
||||
type: gotcha
|
||||
tags: [vue3, pinia, state-management, setup-stores, ssr, devtools]
|
||||
---
|
||||
|
||||
# Return All State Properties in Pinia Setup Stores
|
||||
|
||||
**Impact: HIGH** - When using Pinia's setup store syntax (Composition API style), you MUST return all state properties from the setup function. Private state that isn't returned will break Server-Side Rendering (SSR), Vue DevTools inspection, and Pinia plugins.
|
||||
|
||||
This is a critical gotcha that can cause silent failures in production.
|
||||
|
||||
## Task Checklist
|
||||
|
||||
- [ ] Return ALL reactive state properties from setup stores
|
||||
- [ ] Do not create "private" state by omitting it from the return
|
||||
- [ ] If you need private state, use a prefix convention instead (e.g., `_internal`)
|
||||
- [ ] Test stores with DevTools to verify all state is visible
|
||||
- [ ] Verify SSR hydration includes all necessary state
|
||||
|
||||
## The Problem: Private State in Setup Stores
|
||||
|
||||
```javascript
|
||||
// stores/user.js - WRONG: Private state breaks SSR/DevTools
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// Public state
|
||||
const name = ref('')
|
||||
const email = ref('')
|
||||
|
||||
// "Private" state - NOT returned
|
||||
const authToken = ref('') // Won't be serialized for SSR!
|
||||
const lastFetchTime = ref(null) // Won't appear in DevTools!
|
||||
|
||||
const isLoggedIn = computed(() => !!authToken.value)
|
||||
|
||||
async function login(credentials) {
|
||||
const response = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(credentials)
|
||||
})
|
||||
const data = await response.json()
|
||||
|
||||
authToken.value = data.token // This state won't transfer to client in SSR!
|
||||
name.value = data.name
|
||||
email.value = data.email
|
||||
lastFetchTime.value = Date.now()
|
||||
}
|
||||
|
||||
// WRONG: Not returning authToken and lastFetchTime
|
||||
return {
|
||||
name,
|
||||
email,
|
||||
isLoggedIn,
|
||||
login
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**What breaks:**
|
||||
|
||||
1. **SSR Hydration**: `authToken` and `lastFetchTime` won't be serialized and sent to the client
|
||||
2. **DevTools**: These properties won't appear in the store inspector
|
||||
3. **Plugins**: Persistence plugins won't save these properties
|
||||
4. **Time-travel debugging**: Can't track changes to hidden state
|
||||
|
||||
## The Solution: Return Everything
|
||||
|
||||
```javascript
|
||||
// stores/user.js - CORRECT: All state returned
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// All state properties
|
||||
const name = ref('')
|
||||
const email = ref('')
|
||||
const authToken = ref('')
|
||||
const lastFetchTime = ref(null)
|
||||
|
||||
// Getters
|
||||
const isLoggedIn = computed(() => !!authToken.value)
|
||||
|
||||
// Actions
|
||||
async function login(credentials) {
|
||||
const response = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(credentials)
|
||||
})
|
||||
const data = await response.json()
|
||||
|
||||
authToken.value = data.token
|
||||
name.value = data.name
|
||||
email.value = data.email
|
||||
lastFetchTime.value = Date.now()
|
||||
}
|
||||
|
||||
function logout() {
|
||||
authToken.value = ''
|
||||
name.value = ''
|
||||
email.value = ''
|
||||
}
|
||||
|
||||
// CORRECT: Return ALL state, getters, and actions
|
||||
return {
|
||||
// State
|
||||
name,
|
||||
email,
|
||||
authToken,
|
||||
lastFetchTime,
|
||||
// Getters
|
||||
isLoggedIn,
|
||||
// Actions
|
||||
login,
|
||||
logout
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## If You Need "Private" State
|
||||
|
||||
Use naming conventions instead of actually hiding state:
|
||||
|
||||
```javascript
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// Convention: underscore prefix for "internal" state
|
||||
// Still returned, but signals it's not for external use
|
||||
const _authToken = ref('')
|
||||
const _lastFetchTime = ref(null)
|
||||
|
||||
// Public state
|
||||
const name = ref('')
|
||||
const email = ref('')
|
||||
|
||||
const isLoggedIn = computed(() => !!_authToken.value)
|
||||
|
||||
// Return everything - convention communicates intent
|
||||
return {
|
||||
// "Private" - use with caution
|
||||
_authToken,
|
||||
_lastFetchTime,
|
||||
// Public
|
||||
name,
|
||||
email,
|
||||
isLoggedIn
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Option Stores Don't Have This Problem
|
||||
|
||||
With the Options API syntax, all state is automatically tracked:
|
||||
|
||||
```javascript
|
||||
// stores/user.js - Options syntax: all state is tracked automatically
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
name: '',
|
||||
email: '',
|
||||
authToken: '', // Automatically included
|
||||
lastFetchTime: null // Automatically included
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isLoggedIn: (state) => !!state.authToken
|
||||
},
|
||||
|
||||
actions: {
|
||||
async login(credentials) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## How Setup Stores Map to State
|
||||
|
||||
Understanding the mapping helps avoid mistakes:
|
||||
|
||||
```javascript
|
||||
defineStore('example', () => {
|
||||
// ref() becomes state
|
||||
const count = ref(0) // → state.count
|
||||
|
||||
// computed() becomes getters
|
||||
const double = computed(() => count.value * 2) // → getters.double
|
||||
|
||||
// Regular functions become actions
|
||||
function increment() { // → actions.increment
|
||||
count.value++
|
||||
}
|
||||
|
||||
// CRITICAL: Must return all of them
|
||||
return { count, double, increment }
|
||||
})
|
||||
```
|
||||
|
||||
## Debugging: Verify All State is Returned
|
||||
|
||||
```javascript
|
||||
// Test that DevTools can see all state
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// In DevTools console or tests:
|
||||
console.log(userStore.$state)
|
||||
// Should include ALL reactive state properties
|
||||
|
||||
// Check what's returned
|
||||
console.log(Object.keys(userStore))
|
||||
// Should include: name, email, authToken, lastFetchTime, isLoggedIn, login, logout
|
||||
```
|
||||
|
||||
## Reference
|
||||
- [Pinia - Setup Stores](https://pinia.vuejs.org/core-concepts/#setup-stores)
|
||||
- [Pinia - SSR](https://pinia.vuejs.org/ssr/)
|
||||
- [Mastering Pinia - Common Mistakes](https://masteringpinia.com/blog/top-5-mistakes-to-avoid-when-using-pinia)
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
---
|
||||
title: Use storeToRefs When Destructuring Pinia Stores
|
||||
impact: HIGH
|
||||
impactDescription: Destructuring Pinia stores directly breaks reactivity - state changes won't trigger UI updates
|
||||
type: gotcha
|
||||
tags: [vue3, pinia, state-management, reactivity, destructuring]
|
||||
---
|
||||
|
||||
# Use storeToRefs When Destructuring Pinia Stores
|
||||
|
||||
**Impact: HIGH** - Pinia stores are wrapped with `reactive`, so destructuring them directly extracts non-reactive values. Changes to the store won't be reflected in your component, causing stale UI and confusing bugs.
|
||||
|
||||
This is one of the most common mistakes when using Pinia, especially for developers coming from Vuex or other state management libraries.
|
||||
|
||||
## Task Checklist
|
||||
|
||||
- [ ] Never destructure state or getters directly from a Pinia store
|
||||
- [ ] Use `storeToRefs()` to extract reactive state and getters
|
||||
- [ ] Destructure actions directly (they don't need reactivity)
|
||||
- [ ] Remember: `storeToRefs` is for state/getters, direct destructure is for actions
|
||||
|
||||
## The Problem: Direct Destructuring
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// WRONG: Direct destructuring breaks reactivity
|
||||
const { name, email, isLoggedIn } = userStore
|
||||
|
||||
// Later, when store updates...
|
||||
userStore.login({ name: 'John', email: 'john@example.com' })
|
||||
|
||||
// name and email are still the OLD values!
|
||||
// UI won't update because these are no longer reactive
|
||||
console.log(name) // undefined (or initial value)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- This won't update when store changes -->
|
||||
<div>{{ name }}</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## The Solution: Use storeToRefs
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// CORRECT: Use storeToRefs for state and getters
|
||||
const { name, email, isLoggedIn } = storeToRefs(userStore)
|
||||
|
||||
// Actions can be destructured directly (they're just functions)
|
||||
const { login, logout } = userStore
|
||||
|
||||
// Now when the store updates...
|
||||
login({ name: 'John', email: 'john@example.com' })
|
||||
|
||||
// name and email are reactive refs that update automatically
|
||||
console.log(name.value) // 'John'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- This updates reactively -->
|
||||
<div>{{ name }}</div>
|
||||
<button @click="logout">Logout</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Understanding Why This Happens
|
||||
|
||||
Pinia stores are reactive objects (like `reactive()`). When you destructure:
|
||||
|
||||
```javascript
|
||||
const store = useCounterStore()
|
||||
// store is a reactive Proxy
|
||||
|
||||
const { count } = store
|
||||
// count is now just a primitive number (0), not reactive
|
||||
// It's like doing: const count = 0
|
||||
|
||||
// vs with storeToRefs
|
||||
const { count } = storeToRefs(store)
|
||||
// count is now a ref that stays connected to the store
|
||||
// It's like: const count = computed(() => store.count)
|
||||
```
|
||||
|
||||
## Complete Pattern: State, Getters, and Actions
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
|
||||
const cartStore = useCartStore()
|
||||
|
||||
// State and getters: USE storeToRefs
|
||||
const {
|
||||
items, // state
|
||||
itemCount, // getter
|
||||
totalPrice, // getter
|
||||
isEmpty // getter
|
||||
} = storeToRefs(cartStore)
|
||||
|
||||
// Actions: destructure directly
|
||||
const {
|
||||
addItem,
|
||||
removeItem,
|
||||
clearCart
|
||||
} = cartStore
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isEmpty">Cart is empty</div>
|
||||
<div v-else>
|
||||
<p>{{ itemCount }} items - ${{ totalPrice }}</p>
|
||||
<ul>
|
||||
<li v-for="item in items" :key="item.id">
|
||||
{{ item.name }}
|
||||
<button @click="removeItem(item.id)">Remove</button>
|
||||
</li>
|
||||
</ul>
|
||||
<button @click="clearCart">Clear All</button>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Alternative: Don't Destructure
|
||||
|
||||
If you prefer, you can avoid destructuring entirely:
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
// Use userStore.name, userStore.login(), etc. directly
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>{{ userStore.name }}</div>
|
||||
<button @click="userStore.logout()">Logout</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
This works fine but is more verbose for stores used frequently in the template.
|
||||
|
||||
## Common Mistake: Mixing storeToRefs with Actions
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// WRONG: Don't include actions in storeToRefs
|
||||
// Actions are just functions and storeToRefs will skip them anyway
|
||||
const { name, login } = storeToRefs(userStore)
|
||||
// login is undefined! Actions aren't included in storeToRefs result
|
||||
|
||||
// CORRECT: Separate state/getters from actions
|
||||
const { name } = storeToRefs(userStore)
|
||||
const { login } = userStore
|
||||
</script>
|
||||
```
|
||||
|
||||
## TypeScript Tip
|
||||
|
||||
With TypeScript, the types work correctly:
|
||||
|
||||
```typescript
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// name is Ref<string>, email is Ref<string>
|
||||
const { name, email } = storeToRefs(userStore)
|
||||
|
||||
// login is (credentials: Credentials) => Promise<void>
|
||||
const { login } = userStore
|
||||
```
|
||||
|
||||
## Reference
|
||||
- [Pinia - Destructuring from a Store](https://pinia.vuejs.org/core-concepts/#destructuring-from-a-store)
|
||||
- [Pinia API - storeToRefs](https://pinia.vuejs.org/api/modules/pinia.html#storetorefs)
|
||||
@@ -0,0 +1,238 @@
|
||||
---
|
||||
title: Use URL State for Shareable Filters Instead of Stores
|
||||
impact: MEDIUM
|
||||
impactDescription: Storing filter/search state only in stores loses state on refresh and prevents link sharing
|
||||
type: best-practice
|
||||
tags: [vue3, state-management, url, router, filters, ux, vue-router]
|
||||
---
|
||||
|
||||
# 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
|
||||
|
||||
```vue
|
||||
<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
|
||||
|
||||
```vue
|
||||
<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:
|
||||
|
||||
```vue
|
||||
<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:
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
// GOOD: Clean, readable URLs
|
||||
/products?category=electronics&sort=price&q=phone
|
||||
|
||||
// AVOID: Overly complex URLs
|
||||
/products?filters=%7B%22category%22%3A%22electronics%22%7D
|
||||
```
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
- [VueUse - useRouteQuery](https://vueuse.org/router/useRouteQuery/)
|
||||
- [Vue Router - Query Parameters](https://router.vuejs.org/guide/essentials/passing-props.html#passing-props-to-route-components)
|
||||
- [Mastering Pinia - URL State](https://masteringpinia.com/blog/top-5-mistakes-to-avoid-when-using-pinia)
|
||||
@@ -0,0 +1,262 @@
|
||||
---
|
||||
title: Use Pinia for Large-Scale Vue Applications
|
||||
impact: MEDIUM
|
||||
impactDescription: Hand-rolled reactive stores lack DevTools integration, TypeScript support, and debugging capabilities needed for production apps
|
||||
type: best-practice
|
||||
tags: [vue3, pinia, state-management, devtools, architecture, scalability]
|
||||
---
|
||||
|
||||
# Use Pinia for Large-Scale Vue Applications
|
||||
|
||||
**Impact: MEDIUM** - While Vue's Composition API allows creating simple reactive stores with `reactive()` and `ref()`, these hand-rolled solutions lack the tooling, conventions, and debugging capabilities needed for large-scale production applications. Pinia is the official Vue state management solution and should be used for any non-trivial application.
|
||||
|
||||
## Task Checklist
|
||||
|
||||
- [ ] Use Pinia for applications with shared state across multiple components
|
||||
- [ ] Use Pinia when team collaboration requires consistent patterns
|
||||
- [ ] Use Pinia when you need Vue DevTools state debugging
|
||||
- [ ] Hand-rolled `reactive()` is acceptable only for simple, single-developer apps
|
||||
- [ ] Migrate from Vuex to Pinia (Vuex is in maintenance mode)
|
||||
|
||||
## When Hand-Rolled State is Acceptable
|
||||
|
||||
Simple reactive state is fine for:
|
||||
- Prototypes and proof-of-concepts
|
||||
- Very small applications with minimal shared state
|
||||
- Single-developer projects with limited scope
|
||||
- Learning purposes
|
||||
|
||||
```javascript
|
||||
// Simple hand-rolled store - OK for small apps
|
||||
import { reactive, readonly } from 'vue'
|
||||
|
||||
export const store = reactive({
|
||||
count: 0,
|
||||
increment() {
|
||||
this.count++
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## When to Use Pinia
|
||||
|
||||
Use Pinia when you have any of these requirements:
|
||||
|
||||
### 1. DevTools Integration
|
||||
|
||||
Pinia provides rich Vue DevTools support:
|
||||
- Timeline of state changes
|
||||
- State inspection and editing
|
||||
- Time-travel debugging
|
||||
- Action tracking
|
||||
|
||||
```javascript
|
||||
// Pinia store - fully visible in DevTools
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', {
|
||||
state: () => ({ count: 0 }),
|
||||
actions: {
|
||||
increment() {
|
||||
this.count++ // Tracked in DevTools timeline
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 2. TypeScript Support
|
||||
|
||||
Pinia has excellent TypeScript inference:
|
||||
|
||||
```typescript
|
||||
// Full type inference without extra configuration
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
name: '',
|
||||
age: 0,
|
||||
preferences: {
|
||||
theme: 'light' as 'light' | 'dark'
|
||||
}
|
||||
}),
|
||||
|
||||
getters: {
|
||||
// Return type is inferred
|
||||
displayName: (state) => state.name || 'Anonymous'
|
||||
},
|
||||
|
||||
actions: {
|
||||
// Full parameter type checking
|
||||
setUser(name: string, age: number) {
|
||||
this.name = name
|
||||
this.age = age
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Usage is fully typed
|
||||
const userStore = useUserStore()
|
||||
userStore.name // string
|
||||
userStore.displayName // string
|
||||
userStore.setUser('John', 30) // Type-checked
|
||||
```
|
||||
|
||||
### 3. Team Collaboration
|
||||
|
||||
Pinia enforces conventions that help teams:
|
||||
|
||||
```javascript
|
||||
// Consistent structure across all stores
|
||||
// stores/products.js
|
||||
export const useProductsStore = defineStore('products', {
|
||||
state: () => ({ /* ... */ }),
|
||||
getters: { /* ... */ },
|
||||
actions: { /* ... */ }
|
||||
})
|
||||
|
||||
// stores/cart.js - Same structure
|
||||
export const useCartStore = defineStore('cart', {
|
||||
state: () => ({ /* ... */ }),
|
||||
getters: { /* ... */ },
|
||||
actions: { /* ... */ }
|
||||
})
|
||||
```
|
||||
|
||||
### 4. Hot Module Replacement (HMR)
|
||||
|
||||
Pinia supports HMR out of the box - state persists during development:
|
||||
|
||||
```javascript
|
||||
// State survives code changes during development
|
||||
// No need to re-login or re-create state after every edit
|
||||
```
|
||||
|
||||
### 5. Server-Side Rendering (SSR)
|
||||
|
||||
Pinia handles SSR state correctly:
|
||||
|
||||
```javascript
|
||||
// Automatic per-request state isolation
|
||||
// State serialization for hydration
|
||||
// No cross-request pollution
|
||||
```
|
||||
|
||||
### 6. Plugin Ecosystem
|
||||
|
||||
Pinia supports plugins for common needs:
|
||||
|
||||
```javascript
|
||||
import { createPinia } from 'pinia'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
|
||||
const pinia = createPinia()
|
||||
pinia.use(piniaPluginPersistedstate)
|
||||
|
||||
// Now stores can persist to localStorage
|
||||
export const useSettingsStore = defineStore('settings', {
|
||||
state: () => ({
|
||||
theme: 'light',
|
||||
language: 'en'
|
||||
}),
|
||||
persist: true // Automatically saved/restored
|
||||
})
|
||||
```
|
||||
|
||||
## Pinia vs Hand-Rolled Comparison
|
||||
|
||||
| Feature | Hand-Rolled `reactive()` | Pinia |
|
||||
|---------|-------------------------|-------|
|
||||
| DevTools integration | No | Yes |
|
||||
| TypeScript inference | Manual | Automatic |
|
||||
| HMR support | No | Yes |
|
||||
| SSR support | Manual | Built-in |
|
||||
| Plugins | No | Yes |
|
||||
| Time-travel debugging | No | Yes |
|
||||
| Learning curve | Lower | Slightly higher |
|
||||
| Bundle size | Smaller | ~1KB |
|
||||
| Team conventions | None | Enforced |
|
||||
|
||||
## Migration from Vuex
|
||||
|
||||
Vuex is now in maintenance mode. Migrate to Pinia for new features:
|
||||
|
||||
```javascript
|
||||
// Vuex (legacy)
|
||||
export default createStore({
|
||||
state: { count: 0 },
|
||||
mutations: {
|
||||
INCREMENT(state) { state.count++ }
|
||||
},
|
||||
actions: {
|
||||
increment({ commit }) { commit('INCREMENT') }
|
||||
}
|
||||
})
|
||||
|
||||
// Pinia (recommended)
|
||||
export const useCounterStore = defineStore('counter', {
|
||||
state: () => ({ count: 0 }),
|
||||
actions: {
|
||||
increment() { this.count++ } // No mutations needed!
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Pinia advantages over Vuex:**
|
||||
- No mutations (simpler mental model)
|
||||
- Better TypeScript support
|
||||
- No nested modules complexity
|
||||
- Smaller bundle size
|
||||
- Composition API style available
|
||||
|
||||
## Pinia Store Styles
|
||||
|
||||
Choose the style that fits your team:
|
||||
|
||||
### Options Style (Similar to Options API)
|
||||
|
||||
```javascript
|
||||
export const useCounterStore = defineStore('counter', {
|
||||
state: () => ({ count: 0 }),
|
||||
getters: {
|
||||
double: (state) => state.count * 2
|
||||
},
|
||||
actions: {
|
||||
increment() { this.count++ }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Setup Style (Composition API)
|
||||
|
||||
```javascript
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const double = computed(() => count.value * 2)
|
||||
function increment() { count.value++ }
|
||||
|
||||
return { count, double, increment }
|
||||
})
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
npm install pinia
|
||||
```
|
||||
|
||||
```javascript
|
||||
// main.js
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.mount('#app')
|
||||
```
|
||||
|
||||
## Reference
|
||||
- [Vue.js - State Management](https://vuejs.org/guide/scaling-up/state-management.html)
|
||||
- [Pinia Documentation](https://pinia.vuejs.org/)
|
||||
- [Pinia vs Vuex](https://pinia.vuejs.org/introduction.html#comparison-with-vuex)
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
---
|
||||
title: Include Parentheses When Calling Store Methods in Templates
|
||||
impact: MEDIUM
|
||||
impactDescription: Omitting parentheses on store method calls loses the correct 'this' context, causing unexpected behavior
|
||||
type: gotcha
|
||||
tags: [vue3, state-management, reactive, methods, templates, this-binding]
|
||||
---
|
||||
|
||||
# Include Parentheses When Calling Store Methods in Templates
|
||||
|
||||
**Impact: MEDIUM** - When calling methods on a reactive store object in Vue templates, you must include parentheses (even without arguments) to ensure the method is called with the correct `this` context. Without parentheses, `this` inside the method may not refer to the store.
|
||||
|
||||
This is a subtle gotcha that can cause hard-to-debug issues with hand-rolled reactive stores.
|
||||
|
||||
## Task Checklist
|
||||
|
||||
- [ ] Always use parentheses when calling store methods in templates: `@click="store.increment()"`
|
||||
- [ ] Avoid passing store methods as bare references: `@click="store.increment"` is problematic
|
||||
- [ ] Consider using arrow functions or Pinia (which handles this automatically)
|
||||
- [ ] Test store method calls work correctly with `this` references
|
||||
|
||||
## The Problem
|
||||
|
||||
```javascript
|
||||
// store.js
|
||||
import { reactive } from 'vue'
|
||||
|
||||
export const store = reactive({
|
||||
count: 0,
|
||||
increment() {
|
||||
this.count++ // 'this' should refer to the store
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- WRONG: Without parentheses, 'this' binding may be lost -->
|
||||
<button @click="store.increment">
|
||||
{{ store.count }}
|
||||
</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
When you write `@click="store.increment"` (without parentheses), Vue passes the method as a callback to the event handler. The method gets called without being bound to the store object, so `this` inside `increment()` may be `undefined` or the global object instead of the store.
|
||||
|
||||
## The Solution
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- CORRECT: With parentheses, method is called with correct context -->
|
||||
<button @click="store.increment()">
|
||||
{{ store.count }}
|
||||
</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
With parentheses, you're telling Vue to call `store.increment()` directly, which preserves the `this` context as the store object.
|
||||
|
||||
## Why This Happens
|
||||
|
||||
In JavaScript, when you reference a method without calling it, you get the function itself without its binding:
|
||||
|
||||
```javascript
|
||||
const store = {
|
||||
count: 0,
|
||||
increment() {
|
||||
this.count++
|
||||
}
|
||||
}
|
||||
|
||||
// With parentheses - correct context
|
||||
store.increment() // this === store ✓
|
||||
|
||||
// Without parentheses - getting the function
|
||||
const fn = store.increment
|
||||
fn() // this === undefined (in strict mode) ✗
|
||||
```
|
||||
|
||||
Vue's event handler behavior:
|
||||
- `@click="store.increment"` - Vue receives `store.increment` as a function and calls it later
|
||||
- `@click="store.increment()"` - Vue evaluates `store.increment()` when the event fires
|
||||
|
||||
## Alternative Solutions
|
||||
|
||||
### 1. Use Arrow Functions in the Template
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<button @click="() => store.increment()">
|
||||
{{ store.count }}
|
||||
</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 2. Bind Methods in the Store Definition
|
||||
|
||||
```javascript
|
||||
// store.js
|
||||
import { reactive } from 'vue'
|
||||
|
||||
const state = reactive({
|
||||
count: 0
|
||||
})
|
||||
|
||||
// Methods defined separately, arrow functions don't have 'this' issues
|
||||
export const store = {
|
||||
get count() { return state.count },
|
||||
increment: () => { state.count++ }
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Pinia (Recommended)
|
||||
|
||||
Pinia handles method binding correctly:
|
||||
|
||||
```javascript
|
||||
// stores/counter.js
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', {
|
||||
state: () => ({ count: 0 }),
|
||||
actions: {
|
||||
increment() {
|
||||
this.count++ // Pinia ensures 'this' is correct
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { useCounterStore } from '@/stores/counter'
|
||||
const counter = useCounterStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Both work correctly with Pinia -->
|
||||
<button @click="counter.increment">Works</button>
|
||||
<button @click="counter.increment()">Also works</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 4. Wrap in a Local Function
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { store } from './store'
|
||||
|
||||
// Wrap store method to ensure correct binding
|
||||
function increment() {
|
||||
store.increment()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Now safe to use without parentheses -->
|
||||
<button @click="increment">
|
||||
{{ store.count }}
|
||||
</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
## When This Matters
|
||||
|
||||
This gotcha specifically affects:
|
||||
- Hand-rolled reactive stores using `reactive()`
|
||||
- Methods that reference `this` inside them
|
||||
- Direct method references in templates without parentheses
|
||||
|
||||
It does NOT affect:
|
||||
- Pinia stores (methods are auto-bound)
|
||||
- Arrow function methods (no `this` binding)
|
||||
- Methods that don't use `this`
|
||||
- Method calls with parentheses
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Safe? | Notes |
|
||||
|---------|-------|-------|
|
||||
| `@click="store.method()"` | Yes | Explicit call preserves context |
|
||||
| `@click="store.method"` | No* | Context may be lost |
|
||||
| `@click="() => store.method()"` | Yes | Arrow wrapper preserves context |
|
||||
| `@click="localMethod"` | Yes | Component methods are auto-bound |
|
||||
| Pinia: `@click="store.action"` | Yes | Pinia handles binding |
|
||||
|
||||
*Only problematic if the method uses `this`
|
||||
|
||||
## Reference
|
||||
- [Vue.js State Management - Tip on Method Binding](https://vuejs.org/guide/scaling-up/state-management.html#simple-state-management-with-reactivity-api)
|
||||
- [MDN - this in JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this)
|
||||
Reference in New Issue
Block a user