In Pinia, the order store's placeOrder calls the auth store and auth's logout clears the order store; how do two setup stores use each other safely?
answer
- store registered before setup runs
- the second lookup gets an unfinished store
- no mutual reads in setup bodies
- reads inside actions and computeds
- prefer one direction
basics
~20 sBoth setup stores may call each other's useStore() at the top and call each other's actions. Neither may read the other's state directly in its setup body, because one of them is still unfinished then; read inside actions and computeds.
solid answer
~40 sPinia registers a store under its id **before** running its setup function, precisely so that stores can instantiate each other without an infinite loop. When `useOrderStore()` runs order's setup, which calls `useAuthStore()`, whose setup calls `useOrderStore()`, that inner call finds the registered order store and returns it, unfinished: its state and actions are not attached yet. So reading `order.items` in auth's setup body gives `undefined`, and Pinia's cookbook forbids both stores reading each other's state directly in setup. Calling each other's `useStore()` at the top is fine; the reads and calls move into actions and `computed()`s, which run after both stores are complete. `auth.logout()` calling `order.clear()` and `order.placeOrder()` awaiting `auth.refreshToken()` are both safe. Even so, prefer one direction: a checkout flow that coordinates both stores in one action keeps the graph simple.
code
ts · 17 linesimport { defineStore } from 'pinia'
import { ref } from 'vue'
import { useOrderStore } from './order'
export const useAuthStore = defineStore('auth', () => {
const order = useOrderStore() // reference only: fine
const token = ref<string | null>(null)
// const cartSize = order.items.length <- direct read in setup: not allowed
function logout() {
token.value = null
order.clear() // runs later, both stores complete: fine
}
return { token, logout }
})go deeper
Know that stores can call each other's useStore() and actions, and that setup bodies must not read each other's state.
Explain why the rule exists: the store is registered before setup runs, so a mutual lookup returns an unfinished store.
Diagnose values captured as undefined in setup bodies, keep cross-store reads in actions and computeds, and give stores a clear() action.
Design store dependencies to run one way, with coordination in a higher-level store or composable, so stores stay testable alone.
## How Pinia builds two stores that need each other A setup store is created the first time its `useStore()` is called. Pinia's creation sequence matters here: 1. Create the store object with the built-in `$` methods (`$patch`, `$subscribe`, `$onAction` and so on). 2. **Register it** in the pinia's store registry under its id. The source comment says why: so that the setup of stores can instantiate each other before they are finished, without creating infinite loops. 3. Run the setup function. 4. Attach what setup returned: state, getters, actions. Now take the checkout pair: - `useOrderStore()` registers `order` and runs its setup; - that setup calls `useAuthStore()`, which registers `auth` and runs auth's setup; - auth's setup calls `useOrderStore()`; `order` is already registered, so the call **returns the unfinished order store** instead of starting again. No loop, but for a moment auth holds an order store that has not finished step 4. ## What is safe and what is not | Where auth touches order | When it runs | Safe? | |---|---|---| | `const order = useOrderStore()` at the top of setup | during setup | yes: only a reference | | `order.items` read directly in setup | during setup | no: may be `undefined` | | `order.items` read in a `computed()` | on first read, later | yes | | `order.clear()` called in `logout()` | when logout is called | yes | Pinia's composing-stores cookbook states the rule: stores that use each other cannot **both** directly read each other's state in their setup function. Reading in `computed`s and actions is the documented fix, because by then both setups have returned. ## The checkout pair, done right - `order` calls `useAuthStore()` at the top of setup and, in `placeOrder`, checks `auth.isAuthenticated`, awaits `auth.refreshToken()` and sends `auth.token`. - `auth` calls `useOrderStore()` at the top of setup and, in `logout`, calls `order.clear()` so a signed-out user leaves no cart behind. - Neither setup body reads the other store's state. Note that `order.clear()` is the order store's own action. A setup store has no working built-in `$reset()`, so a store meant to be cleared from outside should offer one. ## Why prefer one direction anyway Mutual dependency works, but it has costs: - every change to one store's shape can break the other; - the creation order becomes something readers must reason about; - testing one store pulls in the other. Common ways to cut the cycle: 1. **Let the lower-level store stay ignorant.** `auth` knows nothing about orders; a `signOut` action in a higher-level `checkout` or `session` store calls `auth.logout()` and then `order.clear()`. 2. **Coordinate in a composable** that uses both stores, if the workflow belongs to a page rather than to shared state. 3. **Pass data, not stores.** `order.placeOrder(token)` receives the token as an argument when the order store should not depend on auth at all. ## How the bug shows up The failure is not always a crash. `order.items.length` in a setup body throws, which is at least visible; `order.items` or `order.status` quietly yields `undefined`, which gets captured in a ref or a closure. And because the unfinished store is whichever one was created first, the behaviour can depend on which store a page happened to use first. When a store only misbehaves on some routes, check its setup body for direct reads of another store. ## Option stores in the same situation Option stores rarely hit the creation-time hazard, because they have no setup body: their `state()` builds plain data, and other stores are looked up inside actions and getters, which only run when called or read. Two option stores whose actions call each other are therefore safe by construction. The hazard returns if `state()` itself calls another store and reads from it, which is also the mistake of copying another store's value into state. ## Review checklist 1. Every `useXStore()` in a setup store sits at the top of the setup function. 2. No setup body reads another store's state or getters directly; reads live in actions and `computed()`s. 3. Stores that must be cleared from outside expose an action for it. 4. Every mutual dependency has a reason; otherwise the coordinating logic moves up a level.
- Do two Pinia option stores that call each other in actions need the same care?Less. Option stores usually call `useOtherStore()` inside actions or getters, which run after both stores exist, so there is no setup body where a direct read could see an unfinished store. The design cost of a mutual dependency remains; only the creation-time hazard is specific to setup stores.
- How would you remove the order-auth cycle in a Pinia checkout?Make the dependency one-way. Keep `auth` unaware of orders, and move sign-out coordination into a higher-level action, for example in a session store, that calls `auth.logout()` and then `order.clear()`. Or pass the token into `order.placeOrder(token)` so the order store does not import auth at all.
saying these in an interview costs you the question
- Two Pinia stores that use each other always cause an infinite loop.
- Pinia builds stores in dependency order, so setup bodies can read each other freely.
- Calling each other's useStore() at the top of setup is the forbidden part.
- Mutual action calls between stores are not allowed.
- A setup store can be cleared from outside with its built-in $reset().