Clean Pinia Store Structure: Proven Best Practices
In large Vue projects, Pinia stores often balloon in size, making maintenance a nightmare. A solid structure fixes this: mutations only through actions, setup syntax, and no side effects inside the store. This keeps things predictable and testable for mid-to-senior developers.
Mutations Only Through Actions
Directly mutating state from components blurs responsibilities. Here's problematic code:
const docStore = useDocumentStore()
const { client } = storeToRefs(docStore)
client.value = null // Scattered mutation logic
Over time, debugging becomes impossible as changes are spread across components. The fix? Centralized actions:
export const useDocumentStore = defineStore('documents', () => {
const client = ref(null)
const resetClient = () => { client.value = null }
return { client, resetClient }
})
// In component
docStore.resetClient()
A single entry point makes it easy to add validation, logging, or analytics without touching code everywhere.
Setup Stores Over Options API
Options API is outdated for stores. Setup style (defineStore(() => { ... })) delivers:
- Transparent reactivity without
this. - Easier testing.
- Seamless composable integration.
Less boilerplate, reactivity just like in components. For new stores, switching is a must—the difference is immediate.
Ditch Watchers in Stores
Watchers inside stores trigger hidden side effects on state changes, complicating debugging. Stores should just hold data, no event dispatching.
Replace watchers with explicit calls from components or composables. Handle route changes, URL parsing, or external events outside, passing ready data via actions.
Persistence via Plugins
Syncing with localStorage shouldn't mix with business logic. Use pinia-plugin-persistedstate or a custom tested wrapper.
Avoid direct localStorage.setItem in actions—it breaks store purity and hinders refactoring.
Stores as Pure Data Holders
Stores handle only reading, saving, and updating data. Move out:
- Router subscriptions.
- Token parsing.
- WebSocket logic.
- Cross-store calculations.
Pre-commit checklist:
- Could I replace this store with a plain file of constants and functions without breaking architecture?
- If not, it's overloaded—extract the logic.
Key Takeaways
- Centralized mutations: Actions over direct ref access.
- Setup syntax: Easier testing, no
this. - No watchers: Side effects out, store stays predictable.
- Separate persistence: Plugins, not in actions.
- Single responsibility: Complex logic to composables.
This approach keeps stores lean, mockable, and refactor-friendly. Projects stay maintainable, and the habit sticks fast.
— Editorial Team
No comments yet.