Pinia is one of the best state management libraries in the Vue ecosystem.
It’s simple, type-safe, and feels natural with the Composition API.
But there’s a subtle problem many teams run into — often without realizing it.
That problem is zombie state.
What Is Zombie State?
Zombie state is state that should no longer exist, but is still alive and affecting your app.
It usually comes from:
- A previous page
- An old user flow
- A completed form
- A finished async request
Yet it continues to:
- Prefill inputs
- Trigger validation
- Affect UI decisions
- Cause "random" bugs
It doesn’t crash your app.
It just quietly causes confusion.
Why Zombie State Happens in Pinia
Pinia stores are:
- Global
- Long-lived
- Singletons by default
This means one important thing:
Navigation away doesn't reset store data
Unless you explicitly reset it, the state stays in memory — even when it no longer makes sense.
A Very Common Example
// useWizardStore.ts
export const useWizardStore = defineStore('wizard', () => {
const step = ref(2)
const email = ref('old@email.com')
return { step, email }
})
What happens
1- user opens the wizard
2- fills some steps
3- navigates away
4- comes back later
The wizard:
- Starts on step 2
- Shows the old email
- Fails validation unexpectedly
The store never reset, that's the zombie state.
Async Zombie State (Even Trickier)
Zombie state can also come from async code.
async function loadUser() {
user.value = await fetchUser()
}
Scenario:
1- User opens page A
2- Request starts
3- User navigates to page B
4- Request from Page A finishes late
Result:
Page B shows data from page A
This is zombie state caused by a late async response.
How Zombie State Shows Up in Real Apps
You might recognize these symptoms:
- Forms already filled when they shouldn’t be
- Validation errors on first render
- Wrong data after navigation
- UI behaving differently after back/forward
- Bugs that disappear on refresh
These are some of the hardest bugs to debug — because they depend on history, not just current state.
The Key Question to Ask
Whenever you create a store, ask:
Who owns this state, and when should it be destroyed?
If the answer isn’t clear, zombie state is likely.
How to Prevent Zombie State in Pinia
1- Add an explicit reset
The simplest and most effective solution
export const useWizardStore = defineStore('wizard', () => {
const step = ref(1)
const email = ref('')
function reset() {
step.value = 1
email.value = ''
}
return { step, email, reset }
})
Use it when the flow ends
onUnmounted(() => store.reset())
Pinia doesn’t reset state for you — you have to be intentional.
2- Reset on context changes
if state depends on route parameters, reset when they change.
watch(
() => route.params.id,
() => store.reset()
)
This prevents old data from leaking into new contexts.
3- Guard Async Requests
A small guard can prevent async zombie state.
let requestId = 0
async function load() {
const id = ++requestId
const data = await fetchData()
if (id !== requestId) return
state.value = data
}
Late responses are ignored instead of overwriting valid state.
4- Separate UI State from Domain State
UI state often becomes zombie state.
Good candidates for stores
-User data
-Orders
-Permissions
Bad candidates
-Input values
-Modal visibility
-Hover or focus state
Keep short-lived UI state close to the component when possible.
Zombie State vs Stale State
They’re not the same.
| Type | Meaning |
|---|---|
| Stale state | Old but still valid |
| Zombie state | Invalid but still active |
Zombie state is dangerous because it looks correct — until it isn’t.
A Simple Rule of Thumb
If state outlives its owner, it becomes a zombie.
Once you start thinking about state ownership and lifecycle, many “random” bugs suddenly make sense.
Final Thoughts
Zombie state isn’t a Pinia problem.
It’s a state lifecycle problem.
Pinia gives us powerful tools — but with that power comes responsibility:
- Define ownership
- Control lifetime
- Reset intentionally
Doing this consistently leads to apps that are:
- Easier to reason about
- Easier to debug
- More predictable for users
If this helps even one developer avoid a hard-to-trace production bug, it’s worth sharing.
Top comments (0)