Voltar ao blog
VueVue3JavaScriptFrontendVuexPiniaGerenciamento de Estado

Vuex vs Pinia: Guia Completo de Gerenciamento de Estado no Vue.js

23 de abril de 2026 às 19:31 - Por Larissa Santos

Vuex vs Pinia: Guia Completo de Gerenciamento de Estado no Vue.js

Se você trabalha com Vue há algum tempo, já deve ter se perguntado: quando usar Vuex e quando usar Pinia? Ou talvez você esteja iniciando um novo projeto e não sabe qual das duas escolher. Esse artigo foi escrito exatamente para responder essas perguntas de forma prática, com exemplos reais e sem enrolação.

Vamos do conceito fundamental até a comparação técnica profunda entre as duas bibliotecas, passando por exemplos de código lado a lado. Ao final, você vai saber exatamente o que cada uma oferece e quando faz sentido usar cada uma.


O que é Gerenciamento de Estado e por que ele importa?

Antes de comparar as ferramentas, vale entender o problema que elas resolvem.

Em aplicações Vue pequenas, o estado de cada componente vive dentro dele mesmo. Um campo de formulário, um toggle de menu, um contador: tudo funciona muito bem com data() ou ref() localmente. O problema começa quando componentes que não possuem relação direta na árvore de componentes precisam compartilhar os mesmos dados.

Imagine um e-commerce com um componente de cabeçalho que exibe o número de itens no carrinho, um componente de lista de produtos que adiciona itens a ele, e uma página de checkout que consome esses mesmos dados. Passar essas informações via props através de múltiplas camadas de componentes (o famoso prop drilling) torna o código frágil e difícil de manter. Usar eventos para comunicar componentes que não são pai e filho é ainda mais trabalhoso.

A solução é centralizar o estado compartilhado em um lugar único, acessível por qualquer componente da aplicação. É exatamente isso que bibliotecas como Vuex e Pinia fazem.

Vale mencionar que a ideia por trás de uma store não é tão diferente de algo que o JavaScript já faz nativamente: closures. Uma função que mantém um estado interno protegido e expõe métodos para manipulá-lo é, na essência, o mesmo princípio que os frameworks formalizaram. Se esse conceito ainda não está claro para você, vale a leitura antes de seguir em frente.


Vuex: a solução consagrada

O Vuex foi por anos a biblioteca oficial de gerenciamento de estado do Vue.js. Inspirado no padrão Flux (criado pelo Facebook) e no Redux (do ecossistema React), ele introduziu um modelo bastante estruturado e previsível para controlar o estado da aplicação.

A arquitetura do Vuex

O Vuex organiza o estado em um único objeto global chamado store, e estabelece regras rígidas sobre como esse estado pode ser modificado. A estrutura é composta por quatro conceitos principais:

State é onde os dados vivem. É o "banco de dados" da sua aplicação no lado do cliente.

Getters são equivalentes a propriedades computadas para o store. Você os usa quando precisa derivar dados do estado, como filtrar uma lista ou calcular um total.

Mutations são as únicas formas permitidas de modificar o estado de forma síncrona. Toda alteração no estado precisa passar por uma mutation, o que torna as mudanças rastreáveis e previsíveis.

Actions lidam com operações assíncronas (como chamadas à API) e, ao final, fazem commit de uma mutation para de fato modificar o estado.

Veja como um store Vuex clássico se parece na prática:

js
import { createStore } from 'vuex'

export default createStore({
  // state: é onde todos os dados da aplicação vivem
  // pense nele como o "banco de dados" do lado do cliente
  state: {
    usuario: null,
    carrinho: []
  },

  // getters: são como computed properties, mas para o store
  // recebem o state como primeiro argumento e retornam um valor derivado
  getters: {
    totalItens(state) {
      return state.carrinho.length
    },
    totalValor(state) {
      // reduce percorre o array somando o preço de cada item
      return state.carrinho.reduce((acc, item) => acc + item.preco, 0)
    }
  },

  // mutations: a ÚNICA forma de modificar o state no Vuex
  // são sempre síncronas e recebem o state como primeiro argumento
  // por convenção, seus nomes são escritos em SNAKE_CASE maiúsculo
  mutations: {
    SET_USUARIO(state, usuario) {
      state.usuario = usuario
    },
    ADICIONAR_AO_CARRINHO(state, produto) {
      state.carrinho.push(produto)
    },
    REMOVER_DO_CARRINHO(state, produtoId) {
      state.carrinho = state.carrinho.filter(item => item.id !== produtoId)
    }
  },

  // actions: lidam com lógica assíncrona (chamadas de API, etc.)
  // recebem um objeto de contexto { commit, state, getters, dispatch }
  // ao final, chamam commit() para acionar a mutation correspondente
  actions: {
    async buscarUsuario({ commit }, userId) {
      const response = await fetch(`/api/usuarios/${userId}`)
      const usuario = await response.json()
      // aqui está o ponto de atenção: a action não altera o state diretamente,
      // ela comita uma mutation que faz isso
      commit('SET_USUARIO', usuario)
    },
    adicionarAoCarrinho({ commit }, produto) {
      commit('ADICIONAR_AO_CARRINHO', produto)
    }
  }
})

E para consumir esse store dentro de um componente Vue 2 com Options API:

js
import { mapState, mapGetters, mapActions } from 'vuex'

export default {
  computed: {
    // mapState mapeia state.usuario e state.carrinho como computed properties
    // agora você acessa via this.usuario e this.carrinho no template
    ...mapState(['usuario', 'carrinho']),

    // mapGetters faz o mesmo para os getters definidos no store
    ...mapGetters(['totalItens', 'totalValor'])
  },
  methods: {
    // mapActions transforma as actions em métodos do componente
    // this.buscarUsuario(id) vai chamar store.dispatch('buscarUsuario', id)
    ...mapActions(['buscarUsuario', 'adicionarAoCarrinho'])
  }
}

Modules: escalando com Vuex

Em projetos maiores, colocar tudo em um único arquivo de store rapidamente se torna inviável. O Vuex resolve isso com modules: é possível dividir o store em fatias menores, cada uma com seu próprio state, mutations, actions e getters.

js
const moduloCarrinho = {
  // namespaced: true isola as mutations e actions deste módulo
  // sem isso, elas ficam no namespace global e podem colidir com outros módulos
  namespaced: true,
  state: () => ({ itens: [] }),
  mutations: {
    ADICIONAR(state, item) { state.itens.push(item) }
  },
  actions: {
    // para chamar esta action de um componente:
    // store.dispatch('carrinho/adicionar', item)
    adicionar({ commit }, item) {
      commit('ADICIONAR', item)
    }
  }
}

const moduloUsuario = {
  namespaced: true,
  state: () => ({ dados: null }),
  mutations: {
    // para chamar esta mutation:
    // store.commit('usuario/SET', dados)
    SET(state, usuario) { state.dados = usuario }
  }
}

export default createStore({
  modules: {
    carrinho: moduloCarrinho,
    usuario: moduloUsuario
  }
})

Com namespaced: true, as actions e mutations ficam acessíveis com o prefixo do módulo: dispatch('carrinho/adicionar', item) ou commit('usuario/SET', dados). Isso evita conflitos de nomes, mas adiciona uma camada de verbosidade que muitos desenvolvedores acham trabalhosa, especialmente em projetos com muitos módulos.

Pontos fortes do Vuex

O Vuex tem uma estrutura muito bem definida, o que é uma vantagem real em equipes grandes. Quando todo mundo segue as mesmas regras (state só é alterado via mutations, lógica assíncrona vai em actions), o código fica previsível. É fácil auditar um bug rastreando qual mutation foi chamada, graças à integração com o Vue DevTools.

Além disso, a comunidade em torno do Vuex é imensa. Anos de produção geraram uma quantidade enorme de tutoriais, soluções no Stack Overflow e padrões documentados para praticamente qualquer cenário.

Fricções que o Vuex carrega

A principal crítica ao Vuex é o boilerplate. Para uma simples atualização de dados, você precisa: definir o estado, criar uma mutation, criar uma action que chama essa mutation, e então dentro do componente fazer dispatch da action. São muitas camadas para uma operação que, conceitualmente, é simples.

Outro ponto é a experiência com TypeScript. O Vuex foi projetado na era do Vue 2, antes de TypeScript se tornar central no ecossistema. Embora seja possível tipar o Vuex, isso exige configurações extras e workarounds que nunca foram elegantes. A inferência de tipos automática simplesmente não está lá.

Por fim, há uma dúvida clássica que assombra desenvolvedores Vuex: devo usar commit ou dispatch? commit chama mutations, dispatch chama actions. Parece simples, mas na prática isso gera confusão, especialmente para quem está aprendendo.


Pinia: o futuro do gerenciamento de estado

O Pinia surgiu como um experimento em 2019, criado por Eduardo San Martin Morote, o mesmo membro do core team do Vue responsável pelo Vue Router. A ideia era reimaginar como um store Vue poderia funcionar com a Composition API em mente desde o primeiro dia.

O experimento deu tão certo que hoje o Pinia é a biblioteca de gerenciamento de estado oficialmente recomendada para Vue.js. A própria documentação do Vuex reconhece isso e orienta novos projetos a usarem Pinia. Evan You, criador do Vue, chegou a se referir ao Pinia como o que seria o Vuex 5.

A arquitetura do Pinia

O Pinia simplifica o modelo do Vuex eliminando as mutations. O ciclo de vida do estado agora tem apenas três conceitos:

State continua sendo os dados da aplicação.

Getters continuam sendo propriedades computadas derivadas do estado.

Actions agora fazem tudo: lógica síncrona, assíncrona, e modificação direta do estado. Não existe mais a distinção entre mutation e action.

Além disso, o Pinia é modular por design. Em vez de um único store global com módulos internos, você cria múltiplos stores independentes, um para cada domínio da sua aplicação.

Veja o equivalente do exemplo anterior, mas em Pinia:

js
import { defineStore } from 'pinia'

// defineStore recebe dois argumentos:
// 1. um ID único (string) que identifica o store na devtools e no sistema de reatividade
// 2. um objeto de configuração (ou uma função, como veremos no Setup Store)
// por convenção, o nome da função começa com "use" e termina com "Store"
export const useCarrinhoStore = defineStore('carrinho', {
  // state agora é uma função que retorna o objeto de dados
  // isso garante que cada instância da aplicação tenha seu próprio estado (importante para SSR)
  state: () => ({
    itens: []
  }),

  // getters funcionam igual ao Vuex: recebem state e retornam um valor derivado
  getters: {
    totalItens: (state) => state.itens.length,
    totalValor: (state) => state.itens.reduce((acc, item) => acc + item.preco, 0)
  },

  // actions aqui fazem TUDO: lógica síncrona, assíncrona e modificação de estado
  // não existe mais a separação entre mutation (síncrono) e action (assíncrono)
  // dentro das actions, "this" se refere ao próprio store
  actions: {
    adicionar(produto) {
      this.itens.push(produto) // modificação direta, sem commit!
    },
    remover(produtoId) {
      this.itens = this.itens.filter(item => item.id !== produtoId)
    }
  }
})

// cada domínio da aplicação tem seu próprio store independente
// não precisamos de módulos aninhados como no Vuex
export const useUsuarioStore = defineStore('usuario', {
  state: () => ({
    dados: null
  }),
  actions: {
    async buscar(userId) {
      const response = await fetch(`/api/usuarios/${userId}`)
      // modificação direta do state dentro da action, sem intermediário
      this.dados = await response.json()
    }
  }
})

E consumindo dentro de um componente com <script setup>:

vue
<script setup>
import { useCarrinhoStore } from '@/stores/carrinho'
import { useUsuarioStore } from '@/stores/usuario'

// chamar o composable retorna a instância reativa do store
// sem mapState, sem mapGetters, sem mapActions
// state, getters e actions ficam acessíveis diretamente no objeto
const carrinho = useCarrinhoStore()
const usuario = useUsuarioStore()

// chamar uma action é tão simples quanto chamar um método normal
carrinho.adicionar({ id: 1, nome: 'Produto', preco: 99.90 })
</script>

<template>
  <div>
    <!-- getters acessados diretamente, sem nenhum helper -->
    <p>Itens no carrinho: {{ carrinho.totalItens }}</p>
    <p>Total: R$ {{ carrinho.totalValor.toFixed(2) }}</p>
  </div>
</template>

Perceba o quanto o código fica mais limpo. Não há necessidade de mapState, mapGetters ou mapActions. Você importa o store, chama a função composable, e acessa estado, getters e actions diretamente.

O estilo Setup Store: máxima flexibilidade

Além da API de objeto (parecida com o estilo Options do Vue), o Pinia também oferece um estilo baseado na Composition API chamado Setup Store. Ele funciona exatamente como um setup(), usando ref, computed e funções normais. Por baixo dos panos, o mecanismo que mantém o estado vivo entre chamadas é o mesmo de uma closure: o ambiente criado quando o store é inicializado persiste enquanto a aplicação estiver rodando, e as funções expostas continuam com acesso a ele.

js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

// no Setup Store, o segundo argumento é uma função (como o setup() de um componente)
// em vez de um objeto com state/getters/actions
export const useCarrinhoStore = defineStore('carrinho', () => {
  // ref() => equivale ao "state" no Options Store
  const itens = ref([])

  // computed() => equivale aos "getters"
  const totalItens = computed(() => itens.value.length)
  const totalValor = computed(() =>
    itens.value.reduce((acc, item) => acc + item.preco, 0)
  )

  // funções normais => equivalem às "actions"
  function adicionar(produto) {
    itens.value.push(produto)
  }

  function remover(produtoId) {
    itens.value = itens.value.filter(item => item.id !== produtoId)
  }

  // tudo que for retornado aqui fica acessível publicamente no store
  // o que não for retornado fica encapsulado (útil para lógica interna privada)
  return { itens, totalItens, totalValor, adicionar, remover }
})

Esse estilo é especialmente poderoso porque permite reutilizar composables dentro do store, compartilhar lógica entre stores com facilidade, e escrever um código que se parece exatamente com um composable Vue comum. Se você quiser ver um exemplo real do Setup Store em um projeto Nuxt 3, escrevi sobre como a useEditorStore foi estruturada no VisitCardGenerator, um gerador de cartões de visita em PDF construído do zero com Nuxt 3 e Pinia.

TypeScript nativo

Uma das maiores vantagens do Pinia é o suporte a TypeScript sem nenhuma configuração adicional. Os tipos são inferidos automaticamente a partir do estado definido. Você não precisa criar interfaces manualmente para o store, nem usar plugins ou hacks para ter autocomplete funcionando:

ts
import { defineStore } from 'pinia'

// interface TypeScript definindo o formato de um produto
// o Pinia vai usar isso para inferir os tipos automaticamente
interface Produto {
  id: number
  nome: string
  preco: number
}

export const useCarrinhoStore = defineStore('carrinho', {
  state: () => ({
    // "as Produto[]" diz ao TypeScript que este array só aceita objetos do tipo Produto
    // a partir daqui, toda a store tem inferência de tipos automática
    itens: [] as Produto[]
  }),
  getters: {
    // a anotação ": number" é necessária aqui por conta de uma limitação do TS
    // quando o getter referencia outro getter via "this" — neste caso é boa prática anotar
    totalValor: (state): number =>
      state.itens.reduce((acc, item) => acc + item.preco, 0)
  },
  actions: {
    // TypeScript já sabe que "produto" deve ser do tipo Produto
    // se você tentar passar um objeto sem o campo "preco", o editor vai reclamar
    adicionar(produto: Produto) {
      this.itens.push(produto)
    }
  }
})

A partir daqui, o TypeScript vai inferir que carrinho.itens é um array de Produto, que carrinho.totalValor é um number, e que carrinho.adicionar espera um objeto do tipo Produto. Tudo isso sem configuração extra.

Modificação direta de estado

Uma funcionalidade que surpreende quem vem do Vuex é que no Pinia é possível modificar o estado diretamente fora de uma action, quando necessário:

js
const carrinho = useCarrinhoStore()

// forma 1: modificação direta de uma propriedade do state
// funciona, mas não é rastreada como uma única operação no DevTools
carrinho.itens = []

// forma 2: $patch com objeto — ideal para mudanças simples em uma ou mais propriedades
// aparece como uma única mutação no Vue DevTools
carrinho.$patch({ itens: [] })

// forma 3: $patch com função — ideal quando a nova mudança depende do estado atual
// recebe o state como argumento e você o modifica diretamente
carrinho.$patch((state) => {
  state.itens = state.itens.filter(item => item.ativo)
})

O método $patch é especialmente útil para aplicar múltiplas mudanças de forma atômica, sem precisar criar uma action específica para cada pequena atualização. Isso não significa que você deva abandonar as actions, elas continuam sendo o lugar ideal para lógica reutilizável e operações assíncronas, mas você tem a flexibilidade de agir diretamente quando faz sentido.

Pinia com Nuxt 3

Para quem usa Nuxt (como é o caso da stack descrita neste blog), o Pinia é suporte oficial. O módulo @pinia/nuxt se integra perfeitamente com SSR, garantindo que o estado não vaze entre requisições de diferentes usuários, um problema real em aplicações server-side rendered. Você pode ver isso funcionando na prática no artigo VisitCardGenerator: gerador de cartões de visita em PDF do zero com Nuxt 3, onde o estado do editor fica inteiramente em uma store Pinia com getters computados, persistência em localStorage e reset de estado.

ts
export default defineNuxtConfig({
  // adicionar o módulo aqui é tudo que você precisa fazer
  // o Pinia fica disponível globalmente, com suporte a SSR configurado automaticamente
  modules: ['@pinia/nuxt']
})

Com esse módulo, os stores do Pinia são automaticamente registrados e funcionam corretamente tanto no servidor quanto no cliente, sem nenhuma configuração adicional para hidratação de estado.


Comparação direta: Vuex vs Pinia

Para deixar as diferenças ainda mais claras, veja uma comparação objetiva dos dois nos principais aspectos:

Estrutura conceitual

ConceitoVuexPinia
StateSimSim
GettersSimSim
MutationsSim (obrigatório)Não existe
ActionsSimSim (tudo vai aqui)
ModulesSim (nested)Múltiplos stores independentes

Instalação e configuração

No Vuex, a instalação é direta, mas a configuração do store exige mais estrutura desde o início:

js
import { createApp } from 'vue'
import { createStore } from 'vuex'
import App from './App.vue'

// createStore recebe toda a configuração de uma vez
// state, mutations, actions, getters e modules ficam centralizados aqui
const store = createStore({ /* ... */ })
const app = createApp(App)
app.use(store) // registra o store globalmente, acessível via this.$store em qualquer componente
app.mount('#app')

No Pinia, a configuração é igualmente simples e os stores são criados de forma independente conforme a necessidade:

js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
// createPinia() inicializa o sistema de stores
// cada store individual é criado e importado separadamente, sob demanda
app.use(createPinia())
app.mount('#app')

Como os stores se comunicam

No Vuex, a comunicação entre módulos pode ser feita via rootState e rootGetters, o que tende a criar acoplamentos não muito explícitos:

js
const moduloA = {
  actions: {
    // para acessar o estado de outro módulo no Vuex,
    // você precisa do rootState que é injetado pelo próprio Vuex no contexto da action
    // isso funciona, mas o acoplamento é implícito: você não sabe de onde vem rootState
    fazAlgo({ commit, rootState }) {
      if (rootState.usuario.autenticado) {
        commit('ATUALIZAR')
      }
    }
  }
}

No Pinia, você simplesmente importa e usa outro store dentro de um store, de forma completamente explícita:

js
import { useUsuarioStore } from './usuario'

export const useCarrinhoStore = defineStore('carrinho', {
  actions: {
    adicionar(produto) {
      // importar e usar outro store é simples e explícito
      // o acoplamento fica visível logo no import, sem mágica
      const usuario = useUsuarioStore()
      if (!usuario.autenticado) {
        throw new Error('Usuário precisa estar logado')
      }
      this.itens.push(produto)
    }
  }
})

Tamanho do bundle

O Pinia pesa aproximadamente 1.5kb gzipado. O Vuex 4 fica em torno de 6.5kb. Para a maioria das aplicações isso não faz diferença prática, mas em projetos onde performance de carregamento é crítica, esse detalhe importa.


Dúvidas frequentes respondidas

"Preciso migrar meu projeto Vuex para Pinia agora?"

Não. Se você tem um projeto Vue 2 ou Vue 3 com Vuex funcionando bem, não há urgência. O Vuex 3 e 4 continuam sendo mantidos. A migração faz sentido quando você está iniciando um novo projeto, quando a dívida técnica do Vuex está atrapalhando a evolução do código, ou quando a equipe quer aproveitar melhor TypeScript e Composition API.

"Dá para usar Pinia com Vue 2?"

Sim, mas com limitações. O suporte a Vue 2 no Pinia foi descontinuado em 2025. Para projetos Vue 2 ativos, o Vuex 3 continua sendo a escolha mais adequada.

"Pinia é mais performático que Vuex?"

Em termos de execução em tempo real, as diferenças são mínimas na maioria dos cenários. O ganho real do Pinia em performance vem da arquitetura modular, que permite ao bundler fazer tree-shaking mais eficiente, carregando apenas os stores que a aplicação realmente usa.

"Posso usar Vuex e Pinia no mesmo projeto?"

Tecnicamente sim. Durante uma migração gradual de Vuex para Pinia em um projeto grande, é possível ter os dois instalados e ir substituindo módulo por módulo. Mas manter os dois permanentemente não é uma prática recomendada.

"As DevTools funcionam igual nos dois?"

Ambos têm integração com o Vue DevTools, mas a experiência é diferente. No Pinia, cada store aparece de forma independente na aba de estado, você consegue visualizar mutations (que no Pinia são as modificações diretas via actions ou $patch), fazer time-travel debugging e inspecionar getters. O Pinia também traz melhorias na visibilidade de quais componentes estão consumindo cada store.


Quando usar cada um

Use Vuex quando:

Você está mantendo um projeto Vue 2 que não tem planos de migração para Vue 3 no curto prazo. Ou quando você tem uma aplicação Vue 3 com uma arquitetura Vuex muito bem estabelecida e migrar não traria ganhos proporcionais ao esforço.

Use Pinia quando:

Você está iniciando qualquer projeto Vue 3 novo. Quando a aplicação usa Nuxt 3. Quando TypeScript é importante para o projeto. Quando a equipe inclui desenvolvedores em diferentes níveis de experiência e você quer um modelo mental mais simples. Em resumo: para tudo que é novo com Vue 3, Pinia é a escolha natural e recomendada.


Migrando de Vuex para Pinia

Se você decidiu migrar, o processo é mais tranquilo do que parece. A documentação oficial do Pinia tem um guia de migração dedicado. A lógica geral é:

Cada módulo Vuex com namespaced: true vira um store Pinia independente. As mutations deixam de existir e sua lógica é absorvida pelas actions. Os getters permanecem praticamente iguais. As actions assíncronas ficam quase idênticas, apenas removendo o parâmetro de context ({ commit, state }) e usando this diretamente.

js
// ------ ANTES: módulo Vuex ------
const moduloVuex = {
  namespaced: true,
  state: () => ({ lista: [] }),
  // mutation obrigatória só para atualizar o state
  mutations: {
    SET_LISTA(state, lista) { state.lista = lista }
  },
  // action que chama a API e depois precisa "comitar" a mutation
  actions: {
    async buscarLista({ commit }) {
      const data = await api.getLista()
      commit('SET_LISTA', data) // duas etapas para uma operação simples
    }
  }
}

// ------ DEPOIS: store Pinia equivalente ------
export const useListaStore = defineStore('lista', {
  state: () => ({ lista: [] }),
  // sem mutation, a action faz tudo diretamente
  actions: {
    async buscarLista() {
      this.lista = await api.getLista() // uma única linha
    }
  }
})

A diferença em termos de linhas de código já é visível em um exemplo simples. Em stores mais complexos, o ganho é ainda maior.


Conclusão

O Vuex foi uma solução sólida que moldou como desenvolvedores Vue pensam sobre estado global por anos. Ele ainda é relevante para projetos legados e situações específicas, mas carrega o peso de uma época em que Composition API e TypeScript não eram prioridades centrais do ecossistema.

O Pinia é o que o Vuex teria sido se tivesse sido projetado hoje. Mais simples, mais leve, mais alinhado com a forma moderna de escrever Vue, e com suporte a TypeScript que realmente funciona sem dor. Não é à toa que o time do Vue o elegeu como a solução oficial.

Para qualquer projeto novo com Vue 3, a resposta é direta: use Pinia. O aprendizado é rápido, a API é intuitiva e você vai escrever código mais limpo desde o primeiro dia. E se quiser ver como tudo isso se aplica em um projeto real, o artigo do VisitCardGenerator mostra cada decisão técnica documentada, do setup da store à geração do PDF.

Referências

Artigos complementares

Feito com e Vue.js
2026 © Larissa Santos