Vue 3 Pinia vs Vuex: Gerenciamento de Estado Moderno e Perguntas de Entrevista 2026

Comparação detalhada entre Pinia e Vuex: design de API, suporte TypeScript, Composition API, desempenho, estratégias de migração e perguntas frequentes em entrevistas Vue para 2026.

Diagrama comparativo de gerenciamento de estado Vue 3 Pinia vs Vuex

O gerenciamento de estado em aplicações Vue 3 passou por uma transformação significativa nos últimos anos. Pinia 3 se estabeleceu como a biblioteca oficial recomendada pela equipe do Vue, enquanto o Vuex permanece em modo de manutenção, sem planos para uma versão 5. Para profissionais que trabalham com Vue em 2026, compreender as diferenças entre essas duas bibliotecas tornou-se essencial tanto para decisões arquiteturais quanto para entrevistas técnicas.

Ponto-Chave

Pinia é a solução oficial de gerenciamento de estado para Vue 3. O Vuex 5 foi cancelado, e Evan You se referiu ao Pinia como o Vuex 5 de fato. Todos os novos projetos Vue 3 devem utilizar Pinia 3.

Diferenças Arquiteturais entre Pinia e Vuex

A diferença arquitetural fundamental entre Pinia e Vuex está na forma como as mutações de estado acontecem. O Vuex impõe um fluxo de dados unidirecional estrito: os componentes despacham actions, as actions fazem commit de mutations, e as mutations modificam o state. O Pinia remove completamente a camada de mutations, permitindo a modificação direta do estado a partir de actions ou até mesmo dos próprios componentes.

Essa simplificação reduz o código repetitivo em aproximadamente 40% na maioria dos projetos. Onde o Vuex exige quatro conceitos (state, getters, mutations, actions), o Pinia funciona com três (state, getters, actions).

| Recurso | Pinia 3 | Vuex 4 | |---------|---------|--------| | Mutations | Nenhuma (alterações diretas de estado) | Obrigatórias para alterar o estado | | TypeScript | Inferência completa, sem augmentation | Augmentation manual de tipos necessária | | Arquitetura de stores | Múltiplas stores planas | Store única com módulos aninhados | | Composition API | Suporte nativo | Baseado na Options API | | Tamanho do bundle | ~1 KB gzipped | ~6 KB gzipped | | Vue Devtools | Suporte completo (v7) | Suporte completo | | SSR | Integrado | Requer configuração | | Hot Module Replacement | Integrado | Configuração manual |

Definindo Stores: Options API vs Sintaxe Setup

O Pinia oferece duas sintaxes para definir stores. A sintaxe Options espelha a estrutura do Vuex, facilitando a migração. A sintaxe Setup aproveita a Composition API do Vue 3 para máxima flexibilidade.

stores/counter-options.tstypescript
import { defineStore } from 'pinia'

// Sintaxe Options Store — familiar para desenvolvedores Vuex
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    lastUpdated: null as Date | null,
  }),
  getters: {
    // Getters recebem state como primeiro argumento com inferência de tipos
    doubleCount: (state) => state.count * 2,
    isPositive(): boolean {
      // Acesso a outros getters via `this`
      return this.count > 0
    },
  },
  actions: {
    increment() {
      // Modificação direta do estado — sem commit() necessário
      this.count++
      this.lastUpdated = new Date()
    },
    async fetchCount(id: string) {
      // Actions assíncronas funcionam sem configuração extra
      const response = await fetch(`/api/counters/${id}`)
      const data = await response.json()
      this.count = data.count
    },
  },
})

A sintaxe Options estabelece uma correspondência direta com os conceitos do Vuex. O state substitui o state do Vuex, os getters permanecem como getters, e as actions absorvem tanto as actions quanto as mutations do Vuex.

stores/counter-setup.tstypescript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// Sintaxe Setup Store — padrões idênticos à Composition API
export const useCounterStore = defineStore('counter', () => {
  // ref() se torna o state
  const count = ref(0)
  const lastUpdated = ref<Date | null>(null)

  // computed() se torna os getters
  const doubleCount = computed(() => count.value * 2)
  const isPositive = computed(() => count.value > 0)

  // Funções se tornam actions
  function increment() {
    count.value++
    lastUpdated.value = new Date()
  }

  async function fetchCount(id: string) {
    const response = await fetch(`/api/counters/${id}`)
    const data = await response.json()
    count.value = data.count
  }

  // É necessário retornar todo o state, getters e actions
  return { count, lastUpdated, doubleCount, isPositive, increment, fetchCount }
})

A sintaxe Setup oferece flexibilidade total: watchers, reutilização de composables e lógica condicional funcionam naturalmente dentro da definição da store. A contrapartida é que cada propriedade reativa e cada método devem ser retornados explicitamente.

Integração TypeScript: Onde o Pinia Supera o Vuex

O suporte a TypeScript é o campo onde o Pinia vence de forma decisiva. O Vuex 4 exige augmentation manual de tipos por meio de declarações de módulos, e módulos aninhados complexos tornam a inferência de tipos frágil. O Pinia infere os tipos automaticamente a partir da definição da store.

typescript
// Vuex 4 — augmentation manual de tipos necessária
import { Store } from 'vuex'

declare module 'vuex' {
  interface State {
    counter: {
      count: number
      lastUpdated: Date | null
    }
  }
}

// Acessar o state requer assertions de tipo ou helpers customizados
const count = (store.state as { counter: { count: number } }).counter.count
typescript
// Pinia — inferência de tipos completa, zero configuração
const counter = useCounterStore()

// TypeScript sabe que counter.count é number
// TypeScript sabe que counter.doubleCount é number
// TypeScript sabe que counter.increment() retorna void
// TypeScript sabe que counter.fetchCount() retorna Promise<void>
counter.increment()
console.log(counter.doubleCount)

O Pinia também suporta stores genéricas para padrões reutilizáveis, uma capacidade que exige boilerplate considerável no Vuex.

Composition API e Integração com Composables

As stores Setup do Pinia aceitam qualquer composable do Vue, habilitando padrões avançados como utilitários compartilhados do VueUse ou estado integrado ao router.

stores/search.tstypescript
import { defineStore } from 'pinia'
import { ref, watch } from 'vue'
import { useDebounceFn } from '@vueuse/core'
import { useRoute } from 'vue-router'

export const useSearchStore = defineStore('search', () => {
  const route = useRoute()
  const query = ref('')
  const results = ref<SearchResult[]>([])
  const isLoading = ref(false)

  // Composable do VueUse utilizado diretamente dentro da store
  const debouncedSearch = useDebounceFn(async (term: string) => {
    if (!term.trim()) {
      results.value = []
      return
    }
    isLoading.value = true
    try {
      const res = await fetch(`/api/search?q=${encodeURIComponent(term)}`)
      results.value = await res.json()
    } finally {
      isLoading.value = false
    }
  }, 300)

  // O watcher sincroniza o parâmetro da URL com o estado da store
  watch(() => route.query.q, (q) => {
    if (typeof q === 'string') {
      query.value = q
      debouncedSearch(q)
    }
  })

  function setQuery(term: string) {
    query.value = term
    debouncedSearch(term)
  }

  return { query, results, isLoading, setQuery }
})

Esse padrão é impossível no Vuex sem soluções alternativas. Os módulos do Vuex não conseguem utilizar composables, watchers ou hooks do router dentro de sua definição. A lógica da store precisa ser dividida entre o módulo Vuex e funções utilitárias externas.

Pronto para mandar bem nas entrevistas de Vue.js / Nuxt.js?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Comunicação entre Stores e Referências Cruzadas

O Vuex utiliza uma store raiz única com módulos nomeados (namespaced). O acesso ao estado entre módulos requer os parâmetros verbosos rootState e rootGetters. As stores do Pinia são independentes por design, e a comunicação entre stores utiliza imports diretos.

stores/cart.tstypescript
import { defineStore } from 'pinia'
import { useProductStore } from './products'
import { useAuthStore } from './auth'
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])

  const total = computed(() => {
    // Acesso a outra store chamando seu composable
    const productStore = useProductStore()
    return items.value.reduce((sum, item) => {
      const product = productStore.getById(item.productId)
      return sum + (product?.price ?? 0) * item.quantity
    }, 0)
  })

  async function checkout() {
    const auth = useAuthStore()
    if (!auth.isAuthenticated) {
      throw new Error('Authentication required')
    }
    // Lógica de checkout
  }

  return { items, total, checkout }
})

Essa arquitetura de stores planas evita as árvores de módulos profundamente aninhados que tornam aplicações Vuex de grande porte difíceis de navegar e refatorar.

Migração do Vuex 4 para o Pinia 3

A migração do Vuex para o Pinia pode ser realizada módulo por módulo. Ambas as bibliotecas podem coexistir durante o período de transição. A estratégia recomendada é começar pelos módulos folha (stores das quais nenhum outro módulo depende) e avançar gradualmente.

Compatibilidade de Migração

Pinia e Vuex podem rodar lado a lado na mesma aplicação. Basta instalar o Pinia junto ao Vuex, migrar um módulo por vez e remover o Vuex somente após a conversão de todos os módulos.

typescript
// Antes: módulo Vuex
// store/modules/user.ts
const userModule = {
  namespaced: true,
  state: () => ({
    profile: null as UserProfile | null,
    preferences: {} as UserPreferences,
  }),
  mutations: {
    SET_PROFILE(state, profile: UserProfile) {
      state.profile = profile
    },
    UPDATE_PREFERENCES(state, prefs: Partial<UserPreferences>) {
      state.preferences = { ...state.preferences, ...prefs }
    },
  },
  actions: {
    async fetchProfile({ commit }) {
      const profile = await api.getProfile()
      commit('SET_PROFILE', profile)
    },
    async updatePreferences({ commit }, prefs: Partial<UserPreferences>) {
      await api.updatePreferences(prefs)
      commit('UPDATE_PREFERENCES', prefs)
    },
  },
  getters: {
    isLoggedIn: (state) => state.profile !== null,
    displayName: (state) => state.profile?.name ?? 'Guest',
  },
}
typescript
// Depois: store Pinia
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { api } from '@/lib/api'

export const useUserStore = defineStore('user', () => {
  const profile = ref<UserProfile | null>(null)
  const preferences = ref<UserPreferences>({})

  const isLoggedIn = computed(() => profile.value !== null)
  const displayName = computed(() => profile.value?.name ?? 'Guest')

  async function fetchProfile() {
    profile.value = await api.getProfile()
  }

  async function updatePreferences(prefs: Partial<UserPreferences>) {
    await api.updatePreferences(prefs)
    Object.assign(preferences.value, prefs)
  }

  return { profile, preferences, isLoggedIn, displayName, fetchProfile, updatePreferences }
})

O padrão de migração é consistente: as mutations se transformam em atribuições diretas dentro das actions, as chamadas a commit() desaparecem, e os helpers mapState/mapGetters são substituídos pela desestruturação do composable da store.

Hidratação SSR com Pinia e Nuxt 4

A renderização do lado do servidor introduz complexidade no gerenciamento de estado. O Pinia lida com a serialização e a hidratação do estado em SSR de forma automática no Nuxt 4, enquanto o Vuex requer o tratamento manual de window.__INITIAL_STATE__.

Armadilha SSR

Stores Setup que utilizam composables como useRoute() ou useFetch() necessitam de tratamento cuidadoso em contextos SSR. Esses composables devem ser chamados somente durante a fase de setup, nunca dentro de callbacks assíncronos.

stores/products.ts — Store Pinia SSR-safe para Nuxt 4typescript
import { defineStore } from 'pinia'

export const useProductStore = defineStore('products', {
  state: () => ({
    items: [] as Product[],
    selectedCategory: 'all',
  }),
  actions: {
    async fetchProducts() {
      // useFetch é específico do Nuxt — usar $fetch para actions da store
      this.items = await $fetch<Product[]>('/api/products', {
        query: { category: this.selectedCategory },
      })
    },
  },
  getters: {
    getById: (state) => (id: string) =>
      state.items.find((item) => item.id === id),
    filteredCount: (state) =>
      state.items.filter((p) => p.inStock).length,
  },
})

O Nuxt 4 serializa automaticamente o estado do Pinia no servidor e o hidrata no cliente. Nenhuma lógica manual de replaceState() ou de desidratação é necessária.

Comparação de Performance e Impacto no Bundle

O peso do Pinia (~1 KB gzipped) é aproximadamente 6 vezes menor que o do Vuex (~6 KB). Em aplicações com code splitting, as stores do Pinia são tree-shakable: stores não utilizadas e suas dependências ficam excluídas dos bundles de produção.

As diferenças de desempenho em tempo de execução são insignificantes para a maioria das aplicações. Ambas as bibliotecas utilizam o sistema de reatividade do Vue internamente. O ganho de performance prático vem da redução do boilerplate, que resulta em menos re-renders causados por assinaturas de estado excessivamente amplas — o modelo de stores planas do Pinia incentiva stores granulares, o que significa que os componentes assinam menos estado.

Com a introdução do Vapor Mode no Vue 3.6, tanto o Pinia quanto o Vuex se beneficiam do pipeline de renderização mais rápido. No entanto, a integração mais estreita do Pinia com a Composition API o posiciona melhor para as otimizações do Vapor Mode.

Perguntas de Entrevista Frequentes sobre Gerenciamento de Estado Vue

Entrevistas técnicas avaliam com frequência os conhecimentos de gerenciamento de estado. A seguir estão as perguntas que mais aparecem em entrevistas para desenvolvedores Vue em 2026.

P: Por que a camada de mutations do Vuex foi removida no Pinia?

As mutations existiam no Vuex para habilitar a depuração com viagem no tempo (time-travel debugging) nos DevTools. Cada alteração de estado precisava passar por uma mutation síncrona para que os DevTools pudessem registrar snapshots. O Pinia alcança a mesma capacidade de depuração rastreando as alterações de estado no nível reativo, tornando a camada de mutations explícita desnecessária. O resultado: menos boilerplate, mesmo poder de depuração.

P: Quando se deve preferir uma store Setup em vez de uma store Options?

As stores Setup são a melhor escolha quando a store precisa de composables (useRoute, useDebounceFn), watchers complexos ou lógica compartilhada proveniente de funções composables externas. As stores Options funcionam bem para state CRUD simples com getters básicos. Na prática, as stores Setup são mais comuns em bases de código de produção porque replicam os padrões da Composition API dos componentes.

P: Como o Pinia lida com a reatividade em objetos aninhados?

O Pinia utiliza reactive() do Vue para o objeto state completo e ref() para propriedades individuais em stores Setup. A reatividade profunda se aplica por padrão — mutações em objetos aninhados são rastreadas automaticamente. Para casos sensíveis a desempenho com grandes conjuntos de dados, shallowRef() permite desativar a reatividade profunda.

P: Explicar as dependências entre stores no Pinia vs Vuex.

No Vuex, o acesso entre módulos utiliza os parâmetros rootState e rootGetters nas actions, criando um acoplamento implícito. No Pinia, as stores se importam diretamente por meio de chamadas a useOtherStore(). Isso torna as dependências explícitas e permite que o TypeScript as verifique em tempo de compilação. Dependências circulares funcionam no Pinia desde que não sejam chamadas durante a fase de setup inicial.

P: O que acontece com o estado do Pinia durante a hidratação SSR?

Durante o SSR, o Pinia serializa todos os estados das stores ativas em JSON e incorpora o resultado no payload HTML. No cliente, o Pinia hidrata cada store antes da montagem dos componentes. Qualquer estado definido durante a renderização do lado do servidor fica disponível imediatamente no cliente sem chamadas de API duplicadas. No Nuxt 4, esse processo é completamente automático.

Para aprofundar a preparação para entrevistas Vue e Nuxt, vale consultar o módulo de entrevista sobre gerenciamento de estado ou revisar o módulo sobre composables Vue.

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Conclusão

  • Pinia 3 é a biblioteca oficial de gerenciamento de estado do Vue — o Vuex está em modo de manutenção sem versão 5 planejada
  • A camada de mutations desapareceu: as actions do Pinia modificam o estado diretamente, reduzindo o boilerplate em aproximadamente 40%
  • A inferência de TypeScript funciona nativamente no Pinia sem nenhuma augmentation, ao contrário das declarações manuais de tipos do Vuex
  • As stores Setup se integram perfeitamente com a Composition API do Vue 3, habilitando a reutilização de composables e watchers dentro das stores
  • A arquitetura de stores planas substitui a árvore de módulos aninhados do Vuex, tornando a comunicação entre stores explícita e type-safe
  • A migração pode ser feita de forma incremental: Pinia e Vuex coexistem, permitindo a conversão módulo por módulo
  • A hidratação SSR é automática no Nuxt 4 com Pinia — sem necessidade de serialização manual
  • Para todos os novos projetos Vue 3 em 2026, o Pinia é a escolha clara graças a uma DX superior, bundles mais leves e suporte mais forte do ecossistema

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Tags

#vue
#pinia
#vuex
#state-management
#interview

Compartilhar

Artigos relacionados