Zombie State in Pinia: A Silent Bug You Might Already Have

Published: (December 19, 2025 at 05:02 PM EST)
3 min read
Source: Dev.to

Source: Dev.to

What Is Zombie State?

  • State that remains after a page, user flow, completed form, or finished async request has finished.
  • It can still:
    • Prefill inputs
    • Trigger validation
    • Influence UI decisions
    • Cause “random” bugs

Because Pinia stores are global, long‑lived singletons by default, navigation away does not 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
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useWizardStore = defineStore('wizard', () => {
  const step = ref(2)
  const email = ref('old@email.com')

  return { step, email }
})
  1. User opens the wizard.
  2. The wizard starts on step 2 and shows the old email.
  3. Validation fails unexpectedly.

The store never resets, creating zombie state.

Async‑Related Zombie State

async function loadUser() {
  user.value = await fetchUser()
}

If the async response arrives after the component has been unmounted or the context has changed, the stale data overwrites the current state, producing the same problem.

Symptoms to Watch For

  • Forms already filled when they shouldn’t be
  • Validation errors on first render
  • Wrong data after navigation (back/forward)
  • UI behaving differently after navigation
  • Bugs that disappear on page refresh

These bugs are hard to debug because they depend on navigation history rather than the current state.

Preventing Zombie State

1. Add an Explicit Reset

// useWizardStore.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useWizardStore = defineStore('wizard', () => {
  const step = ref(1)
  const email = ref('')

  function reset() {
    step.value = 1
    email.value = ''
  }

  return { step, email, reset }
})
import { onUnmounted } from 'vue'
import { useWizardStore } from '@/stores/useWizardStore'

const store = useWizardStore()
onUnmounted(() => store.reset())

Pinia won’t reset state for you—you must be intentional.

2. Reset on Context Changes

import { watch } from 'vue'
import { useRoute } from 'vue-router'
import { useWizardStore } from '@/stores/useWizardStore'

const route = useRoute()
const store = useWizardStore()

watch(
  () => route.params.id,
  () => store.reset()
)

This prevents old data from leaking into new contexts.

3. Guard Async Requests

let requestId = 0

async function load() {
  const id = ++requestId
  const data = await fetchData()

  if (id !== requestId) return // ignore stale response
  state.value = data
}

Late responses are ignored instead of overwriting valid state.

4. Separate UI State from Domain State

  • Good candidates for stores: user data, authentication info, shared domain entities.
  • Bad candidates for stores: short‑lived UI state (e.g., modal visibility, temporary form fields). Keep those close to the component.

Types of Stale State

TypeMeaning
Stale stateOld but still valid
Zombie stateInvalid but still active

Conclusion

Zombie state is dangerous because it looks correct until it isn’t. When state outlives its owner, it becomes a zombie. The issue isn’t specific to Pinia; it’s a state‑lifecycle problem.

Pinia gives us powerful tools, but with that power comes responsibility:

  • Define ownership
  • Control lifetime
  • Reset intentionally

Applying these practices leads to applications 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.

Back to Blog

Related posts

Read more »