When should a Vue 3 team stop hand-rolling module-level reactive stores and adopt a store library?
answer
- the docs list four reasons
- devtools, hot reload, SSR
- conventions scale, clever code does not
- same shape, easy migration
basics
~20 sAdopt a store library when shared state needs things the hand-rolled pattern lacks: team conventions, devtools inspection and time travel, hot reload that keeps state, and per-request SSR isolation. Until then, module stores with readonly views and actions are enough.
solid answer
~40 sA module-level `reactive()` store with a `readonly()` view and action functions covers small apps and isolated slices of state well. The Vue docs list what it lacks at scale: **stronger conventions** for a team, **devtools integration** (timeline, in-component inspection, time-travel debugging), **hot module replacement** that keeps state, and **SSR support**. I would switch when those needs are real: several teams writing stores in different styles, bugs that need a timeline of mutations, an SSR rollout that would otherwise force each store into a factory, or plugin needs such as persistence applied across stores. I would not switch for a single shared value in a client-only app. Because the hand-rolled shape is already state plus actions, migrating is mostly mechanical, so waiting costs little.
go deeper
Know that simple shared state can live in a module-level reactive store and that store libraries add tooling and conventions on top.
List what a hand-rolled store lacks: devtools integration, hot reload that keeps state, SSR per-request instances and enforced conventions.
Tie the switch to concrete triggers in your app, such as an SSR rollout or cross-cutting persistence, and plan a store-by-store migration.
Own the policy: when zero dependencies stops being worth inconsistent stores and missing tooling, and how to keep hand-rolled stores migration-ready.
## What the hand-rolled pattern already gives you In Vue 3, shared state does not need a library. A module can hold a `reactive()` object, export a `readonly()` view of it, and export action functions that are the only writers. Components import the view and the actions. This provides: - a single source of truth with fine-grained, tracked updates; - centralised mutation logic that is easy to search and validate; - plain TypeScript and no dependency. For a client-only app with a handful of shared values (a notification queue, the current user, a theme), that is often the right amount of machinery. ## What it does not give you The Vue documentation lists the things to consider in large-scale production apps, which a hand-rolled store does not provide on its own: | Need | Hand-rolled module store | Store library | |---|---|---| | Team conventions | whatever each author invents | one enforced shape for state, getters, actions | | Devtools | no store view | timeline, inspection, time-travel debugging | | Hot module replacement | editing the store module can reset its state | state preserved across edits | | Server-side rendering | singleton shared across requests | store instance per app, per request | | Cross-cutting behaviour | wire it into every store by hand | plugins applied to all stores | Pinia is the library the Vue docs recommend for new applications; Vuex, the previous official library, is in maintenance mode. ## Signals that it is time Treat these as triggers rather than a checklist to reach: 1. **SSR is planned or live.** Every module store would have to become a factory created per request and provided to the app. A library designed around a per-app instance removes that retrofit. 2. **Several teams write shared state.** Hand-rolled stores drift into different styles (methods on the object, exported refs, singleton composables). A library's conventions make any store readable to anyone. 3. **Debugging needs history.** When a bug report is "the queue was wrong at some point", a mutation timeline in devtools is worth more than any amount of `console.log`. 4. **Cross-cutting features arrive.** Persisting to storage, resetting all stores on logout, or logging every action is a plugin in a library and copy-paste without one. 5. **State is lost during development.** If hot reload keeps wiping carefully built state, developer time is being spent. ## Signals that it is not - The shared state is one or two values and the app renders only in the browser. - The team is small and the stores already follow the readonly-plus-actions shape. - Adding a dependency would mean adding it for a single use. ## Cost of each choice Staying hand-rolled costs little until the triggers above appear, and then each costs repeatedly (every SSR bug, every inconsistent store). Adopting a library costs one dependency, some learning, and a migration. Because the hand-rolled shape is already **state plus actions**, migration is largely mechanical: private state becomes store state, action functions become store actions, module-level `computed`s become getters, and components swap an import for the store's accessor. ## A pragmatic policy - Start with module stores for small, client-only shared state, using readonly views and actions from day one. - Wrap access in a `useX()` function, so the implementation can change without touching callers. - Adopt the library when the first real trigger appears, and migrate store by store rather than in one big change. - Do not mix styles indefinitely: once the library is in, new shared state goes there. ## A migration sketch For the notification queue, the move is short: 1. Create a store with the same state (`items`), the same actions (`notify`, `dismiss`) and the unread count as a getter. 2. Change the body of `useNotifications()` to return the library store's members. 3. Leave components untouched, because they already call `useNotifications()`. 4. Delete the old module state once nothing imports it. ## What interviewers look for This is a judgement question. A strong answer names concrete capabilities (devtools, HMR, SSR, conventions), ties the decision to the app's actual constraints, and shows that the hand-rolled pattern was designed to make the move cheap. A weak answer either says "always use a library" or "libraries are unnecessary in Vue 3" without saying what changes at scale.
- Does adopting a store library remove the need for readonly views and action functions?The library supplies the structure instead: state, getters and actions are defined in one declared shape, and devtools records action calls. The discipline is the same one the readonly-plus-actions pattern expresses; the library makes it the default rather than something each author remembers.
- Your client-only app has one shared notification queue. Would you add a store library for it?Probably not. A module-level reactive queue with a readonly view and notify/dismiss actions covers it without a dependency. I would wrap it in useNotifications() so that moving it into a library later, if SSR or more shared state arrives, does not touch the components.
saying these in an interview costs you the question
- Vue 3 apps always need a store library for any shared state.
- Module-level reactive stores are all a large SSR app will ever need.
- A store library changes nothing except syntax, so it never pays off.
- Migrating from hand-rolled stores means rewriting every component.
- The choice should be made by package popularity, not by the app's needs.