Vue / Intermediate: feature state and effect ownership / Lesson 9
Shared state, Pinia, and ownership boundaries
Choose local or shared state by lifetime and consumers, then expose a store without losing reactivity or mixing server requests.
On this page
Lesson goals
- Decide which state needs a shared owner.
- Use Pinia state, getters, actions, and storeToRefs deliberately.
Promote state only when its contract requires it
Not every form field belongs in a global store. A draft used by one component can remain local, while selected account context or a shared notes collection may need a broader owner. Decide who reads, who changes, and how long the value should survive. A store does not automatically make data durable, private, synchronized, or valid. Keep confirmed records, transient UI choices, and remote request status distinguishable. Prop and event composition can solve small sharing needs without introducing a store. The right boundary makes ownership and reset behavior easier to explain, especially at logout or task completion.
Expose state and operations through a small store
Pinia’s setup-store style uses Vue refs, computed values, and functions returned from defineStore. This lesson uses the documented API compatible with the Vue 3 baseline; install and pin a compatible Pinia release in the actual app. The store id should be unique within the application. Actions can centralize domain rules, but Pinia does not prohibit all direct state mutation, so team conventions and tests still matter. The example selects only an existing note id and returns its reactive state and derived selection. It contains no persistence or network client.
import { ref, computed } from "vue"
import { defineStore } from "pinia"
export const useNotesStore = defineStore("notes", () => {
const notes = ref([{ id: "n1", title: "Reactivity" }])
const selectedId = ref(null)
const selected = computed(() => notes.value.find(note => note.id === selectedId.value) ?? null)
function selectNote(id) {
if (!notes.value.some(note => note.id === id)) return false
selectedId.value = id
return true
}
return { notes, selectedId, selected, selectNote }
})
Requires Vue and Pinia packages plus an app-installed Pinia instance. Initially selected is null; selectNote("n1") returns true and selects the existing note. This framework fragment was not executed against Pinia.
Preserve state access when destructuring
A Pinia store is reactive, and copying its primitive properties by ordinary destructuring can disconnect local bindings from future updates. storeToRefs extracts state and getters as refs while excluding methods and nonreactive properties. Actions can be extracted according to the store API without using the same ref conversion. The example keeps selected state reactive in component setup. Do not apply Vue 3.5’s defineProps compiler rule to a store: these are different mechanisms. Keep the original store available when that is clearer, and avoid creating a second store-shaped object merely for convenience.
import { storeToRefs } from "pinia"
import { useNotesStore } from "./stores/notes.js"
const store = useNotesStore()
const { notes, selected } = storeToRefs(store)
const { selectNote } = store
selectNote("n1")
Use under an installed active Pinia context, typically synchronous component setup. notes and selected retain ref access in JavaScript. This excerpt does not create that application context or claim a store runtime test.
Treat persistence and remote truth as separate systems
A store can cache the current view of data while a backend or durable local database remains authoritative. Decide how loading, stale data, optimistic changes, failures, and account changes affect that view. Browser storage is not automatically secure and can expose sensitive state to client code; do not persist secrets simply because a plugin makes serialization easy. Version and validate restored state before accepting it. Reset account-scoped data on the appropriate lifecycle and ensure pending work cannot apply another user’s result. A successful store assignment is not evidence that a server accepted the change.
Create request-safe owners during SSR
A module-level mutable singleton can leak state between server requests if reused across users. Server rendering therefore needs a fresh application and store context per request, with controlled serialization and hydration. Client-only examples must not be generalized into a server-wide global store. Pinia supports SSR patterns, but integration details depend on the framework and serializer. Test independent synthetic request contexts and avoid embedding private data in public HTML or logs. Keep store ownership, state transfer, and identity boundaries explicit so convenience in component code does not create a cross-user data problem.
Common errors and fixes
A destructured store value stops updating
- Why it happens
- Ordinary destructuring copied a primitive value.
- How to fix it
- Use storeToRefs or retain store property access.
- Prevent it next time
- Distinguish state refs from extracted actions.
A global store leaks between server requests
- Why it happens
- Mutable module state is reused across users.
- How to fix it
- Create a fresh request-owned app and Pinia context.
- Prevent it next time
- Test cross-request isolation explicitly.
A local store update is announced as a saved record
- Why it happens
- Client state is confused with durable remote acceptance.
- How to fix it
- Model pending and confirmed outcomes separately.
- Prevent it next time
- Define the authoritative data source.
Check your understanding
1. Which belongs in a shared store: a one-dialog draft or account context used by many routes?
Show worked solution
The account context often needs a shared owner and clear reset policy. The draft can remain local unless its required lifetime or consumers justify promotion. Store placement follows the contract rather than a rule that every ref must be global.
2. A persistence plugin restores a previous account’s selection. What must be checked?
Show worked solution
Validate account scope, schema version, current permissions, and whether the selected record still exists. Reject stale or unauthorized state and reset deliberately. Serialization convenience does not establish identity isolation.
Key takeaways
- State scope follows ownership, consumers, and lifetime.
- storeToRefs preserves the reactive state interface.
- Durability and SSR isolation require separate design.