{{ state.data.name }}
{{ state.error.message }}
```
- Wrap shared reactive state in `defineQuery()` to prevent desynchronization — regular composables recreate refs for each component instance, causing only the first component to successfully trigger key-based reactivity [source](./references/docs/advanced/reusable-queries.md)
```ts
export const useFilteredTodos = defineQuery(() => {
const search = ref('')
const query = useQuery({
key: () => ['todos', { search: search.value }],
query: () => fetchTodos(search.value),
})
return { ...query, search }
})
```
- Combine hierarchical key factories with `defineQueryOptions()` for strict type safety — this enables automatic type inference in `queryCache` methods without manual type casting or string-based key typos [source](./references/docs/guide/query-keys.md)
```ts
export const todoOptions = defineQueryOptions((id: string) => ({
key: ['todos', id],
query: () => fetchTodo(id),
}))
// Inferred TData: queryCache.getQueryData(todoOptions('1').key)
```
- Handle side effects via `watch` or global plugins instead of query options — `useQuery` intentionally lacks `onSuccess`/`onError` to prevent side-effect duplication across multiple component instances [source](./references/docs/cookbook/query-hooks.md)
- Prefer `refresh()` over `refetch()` for standard UI updates — `refresh()` respects `staleTime` and deduplicates in-flight requests, whereas `refetch()` forces a network call regardless of cache status [source](./references/docs/guide/queries.md)
- Use the `meta` property for declarative cross-cutting concerns — attach metadata to queries to drive global UI behavior (like toast messages) within the `PiniaColadaQueryHooksPlugin` [source](./references/docs/cookbook/query-hooks.md)
- Verify cache state before performing optimistic rollbacks — always check if the current cache value matches the optimistic value in `onError` to avoid overwriting concurrent successful updates from other mutations [source](./references/docs/guide/optimistic-updates.md)
```ts
onError(err, vars, { newTodo, oldTodo }) {
if (newTodo === queryCache.getQueryData(['todos'])) {
queryCache.setQueryData(['todos'], oldTodo)
}
}
```
- Use `queryCache.setEntryState()` for manual status synchronization — this is the preferred way to manually update an entry as setting data to `undefined` via `setQueryData()` is no longer supported for state resets [source](./references/releases/CHANGELOG.md)
- Explicitly import `useRoute` from `vue-router` in Nuxt `defineQuery` definitions — the Nuxt auto-imported version can cause unnecessary query triggers or `undefined` values due to Suspense integration [source](./references/docs/advanced/reusable-queries.md)
- Use the `enabled` getter to guard "immortal" queries in global stores — prevents queries inside Pinia stores from making invalid network requests when required reactive parameters (like route params) are absent [source](./references/docs/guide/queries.md)
```ts
const result = useQuery({
key: () => ['deck', route.params.id],
query: () => fetchDeck(route.params.id),
enabled: () => !!route.params.id,
})
```