Tabela paginada reativa em Vue 3: Composition API e Pinia passo a passo
Por John Pires · 12 de março de 2026

Tabela paginada reativa em Vue 3: Composition API + Pinia
O que é e por que importa
Uma tabela paginada reativa é um componente que exibe um conjunto de dados divididos em páginas e atualiza automaticamente sua exibição quando o estado muda. Em aplicações modernas isso importa porque dados volumosos não devem ser renderizados de uma vez; paginar melhora desempenho, experiência do usuário e reduz custos de rede. Além disso, quando a tabela é reativa e gerenciada por um estado global (Pinia), facilita compartilhamento entre componentes e mantém a lógica previsível.
Para iniciantes: imagine uma prateleira de livros com divisórias. Cada divisória é uma página de resultados. Você não precisa abrir toda a estante para pegar um livro — só a divisória atual. O Vue e o Pinia cuidam de abrir a prateleira certa e avisar componentes quando trocar de divisória.
Neste artigo você verá conceitos do zero, analogias simples, um passo a passo com código comentado e um exemplo prático completo que pode ser copiado e executado em um projeto Vue 3.
O problema que resolvemos
Sem paginação, uma tabela pode tentar renderizar milhares de linhas, travando o navegador. Sem gestão de estado central, múltiplos componentes podem duplicar a lógica de paginação. A solução: um componente de tabela simples + uma store Pinia que controla página atual, tamanho da página, filtros e busca.
Quando usar client-side vs server-side
Existem dois cenários: dados já carregados no cliente (client-side pagination) ou dados paginados pela API (server-side). Para iniciantes focaremos em um padrão híbrido: uma store que pode trabalhar com ambos, com um mock de API que retorna páginas. O ajuste para chamadas reais é trivial.
Conceitos fundamentais com analogias
Antes de codificar, precisamos entender alguns conceitos do Vue 3, Composition API e Pinia. Vou explicar com analogias e depois mostrar como aplicá-los.
Reatividade (analogia: plantas e regadores)
Reatividade no Vue é como ter plantas em vasos que reagem quando você rega: se um valor mudar (você rega), qualquer template ou função que dependa desse valor será automaticamente atualizado (a planta cresce). No Composition API usamos ref e reactive para declarar esses valores reativos.
Exemplo prático: ref para um contador simples, reactive para um objeto complexo com filtros. Os componentes que usam esses valores atualizam instantaneamente.
Pinia (analogia: armário de ferramentas)
Pinia é o armário onde guardamos ferramentas compartilhadas entre componentes. Em vez de cada componente ter sua própria régua, todos pegam a régua do armário. A store centraliza estado, getters e ações assíncronas (como buscar a página X da API).
Vantagens: teste mais fácil, lógica consolidada, composição com Composition API e tipagem se usar TypeScript.
Passo a passo com código comentado
Vamos montar um projeto mínimo com Vue 3 e Pinia. Abaixo estão três blocos de código principais: a store Pinia, um mock de API interno e o componente de tabela paginada. Cada bloco tem contexto antes e explicação depois.
1) Configuração do Pinia e main.js
Contexto: arquivo de inicialização da aplicação. Aqui criamos a instância do Vue, registramos o Pinia e montamos o app.
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
O que isso faz: inicializa o Pinia para que qualquer componente possa usar stores. Depois montamos a aplicação.
2) Store Pinia: controle de paginação
Contexto: a store contém estado reativo da tabela, getters e ações para carregar páginas. Usamos um mock de API (função fetchPage) para simular chamadas. A store expõe ações: setPage, setPageSize, fetchPage.
// stores/useTableStore.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// Mock simples de dados: 100 itens
const ITEMS = Array.from({ length: 100 }, (_, i) => ({ id: i + 1, name: 'Item ' + (i + 1) }))
// Função que simula uma API que retorna uma página
function fetchPageFromMock(page, pageSize, delay = 200) {
const start = (page - 1) * pageSize
const end = start + pageSize
const items = ITEMS.slice(start, end)
return new Promise(resolve => {
setTimeout(() => {
resolve({ items, total: ITEMS.length })
}, delay)
})
}
export const useTableStore = defineStore('table', () => {
const page = ref(1)
const pageSize = ref(10)
const items = ref([])
const total = ref(0)
const loading = ref(false)
const error = ref(null)
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
async function fetchPage(p = page.value) {
loading.value = true
error.value = null
try {
const res = await fetchPageFromMock(p, pageSize.value)
items.value = res.items
total.value = res.total
page.value = p
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
function setPage(p) {
const clamped = Math.min(Math.max(1, p), totalPages.value)
if (clamped !== page.value) fetchPage(clamped)
}
function setPageSize(size) {
pageSize.value = size
// ao mudar pageSize, volte para primeira página por previsibilidade
fetchPage(1)
}
return { page, pageSize, items, total, loading, error, totalPages, fetchPage, setPage, setPageSize }
})
O que acontece: a store mantém estado centralizado. fetchPageFromMock simula latência. A store expõe funciones para alterar página e tamanho da página. Componentes usarão essas funções para mudar a exibição.
3) Componente: TabelaPaginated.vue (template + script)
Contexto: componente que consome a store e renderiza a tabela, controles de paginação e feedback de carregamento.
// components/TabelaPaginated.vue
<template>
<div>
<div style='display:flex;align-items:center;gap:8px;margin-bottom:12px;'>
<label>Linhas por página:
<select v-model='localPageSize'>
<option v-for='n in [5,10,20,50]' :key='n' :value='n'>{{ n }}</option>
</select>
</label>
<div v-if='loading' style='color:#007bff'>Carregando...</div>
<div v-if='error' style='color:#c00'>Erro: {{ error.message || error }}</div>
<div style='margin-left:auto'>Total: {{ total }}</div>
</div>
<table border='1' cellpadding='6' cellspacing='0' style='width:100%;border-collapse:collapse'>
<thead>
<tr>
<th>ID</th>
<th>Nome</th>
</tr>
</thead>
<tbody>
<tr v-for='row in items' :key='row.id'>
<td>{{ row.id }}</td>
<td>{{ row.name }}</td>
</tr>
</tbody>
</table>
<div style='display:flex;align-items:center;gap:6px;margin-top:12px;'>
<button @click='prev' :disabled='page <= 1'>Anterior</button>
<span>Página {{ page }} / {{ totalPages }}</span>
<button @click='next' :disabled='page >= totalPages'>Próxima</button>
</div>
</div>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
import { useTableStore } from '../stores/useTableStore'
const store = useTableStore()
const { items, page, pageSize, total, loading, error, totalPages, fetchPage, setPage, setPageSize } = store
// bind local select com debounce simples
const localPageSize = ref(pageSize)
watch(localPageSize, (v) => {
setPageSize(v)
})
function prev() { setPage(page - 1) }
function next() { setPage(page + 1) }
onMounted(() => {
// Carrega a primeira página automaticamente
if (items.length === 0) fetchPage(1)
})
</script>
O que acontece: o componente lê o estado da store, renderiza os items e controla paginação via setPage. Mudanças em localPageSize acionam setPageSize que recarrega a store.
Exemplo prático completo
Aqui organizo os arquivos mínimos para você copiar para um projeto Vite+Vue 3. Estrutura sugerida:
- src/main.js
- src/stores/useTableStore.js
- src/components/TabelaPaginated.vue
- src/App.vue (apenas monta o componente)
Arquivo App.vue simples para testar o componente:
// App.vue
<template>
<div style='padding:16px'>
<h2>Exemplo: Tabela Paginada</h2>
<TabelaPaginated />
</div>
</template>
<script setup>
import TabelaPaginated from './components/TabelaPaginated.vue'
</script>
Instruções práticas para rodar localmente:
- Crie um projeto com npm init vite@latest e escolha vue
- Instale dependências: npm install e npm install pinia
- Copie os arquivos acima e rode npm run dev
Resultado: uma tabela com paginação funcional usando Pinia e Composition API. A store abstrai fetch e estado; o componente é apenas uma camada de exibição e controle.
Como adaptar para uma API real
Troque fetchPageFromMock por fetch('/api/items?page=' + page + '&size=' + pageSize) e trate headers para total se necessário. A store permanece praticamente igual.
Performance e UX
Use loading para bloquear UI, debounce em buscas, e cache de páginas se precisar evitar refetch constantes. Para tabelas grandes, avalie virtual scrolling em vez de paginação se a navegação for por posição contínua.
Erros comuns, armadilhas e dicas práticas
Essa seção lista problemas que iniciantes enfrentam e como evitá-los.
Erro: manipular estado local e global duplicadamente
Muitos iniciantes mantêm page tanto no componente quanto na store e ficam fora de sincronia. Regra prática: uma única fonte de verdade. Deixe page na store; componentes só chamam ações.
Erro: não clamar limites de página
Se você pedir page 0 ou page > totalPages, a API pode responder vazio ou com erro. Sempre normalize a página usando Math.min/Math.max na store antes de fetch.
Dica: centralize validação e fetch na store. Componentes só disparam ações; isso reduz bugs de sincronização.
Erro: esquecer indicador de carregamento
Sem loading, usuários clicam em botões repetidamente e disparam múltiplas requests. Marque loading=true enquanto a requisição estiver em progresso e desabilite controles relevantes.
Outras armadilhas
- Não faça lógica de paginação no template — coloque na store ou em composables.
- Se usar watch com deep objects, prefira watchers específicos para evitar reruns caros.
- Ao usar APIs reais, trate falhas de rede e timeouts.
Próximos passos para aprofundar
Depois de dominar esse padrão básico, siga para tópicos que elevarão sua tabela paginada a produção:
- Server-side pagination com cursor-based pagination (mais eficiente que offset para grandes datasets)
- Composables reutilizáveis para lógica de paginação e busca (ex: usePagination)
- Virtual scrolling para renderizar somente linhas visíveis
- Cache por página e invalidation inteligente
- Testes unitários da store com mocking de fetch
Sugestão técnica: crie um composable usePagedFetch que encapsula fetch, cache, retries e expõe uma interface consistente para a store ou componentes.
Conclusão prática
Resumo direto: use Pinia como fonte única de verdade para estado de paginação; mantenha componentes focados em renderização e controles; trate loading, erros e normalização de página na store. Esse padrão evita duplicação e facilita evolução do componente.
Passos concretos para aplicar agora:
- Implemente a store com fetch centralizado (como no exemplo).
- Crie um componente leve que injete a store e apenas exponha controles e template.
- Adapte fetch para a API real e adicione tratamento de erros e caches simples.
Com isso você terá uma tabela paginada reativa, fácil de testar, estender e integrar em aplicações reais. Evite misturar responsabilidades; mantenha o store responsável por estado e fetch. Boas práticas simples garantem código robusto e manutenção tranquila.
Assuntos: Vue.js, pinia, composition-api, pagination, reactivity, components
Leia também
useServerQuery no Nuxt 3: composable SSR que deduplica, cache e revalida
Composble server-only para Nuxt 3: deduplicação de requisições, cache por processo e stale-while-revalidate prático. Código real, testes e armadilhas para…
Virtualização reativa em Vue 3: Composition API + Pinia para listas gigantes
Prático e direto: como substituir o anti-pattern de renderizar tudo por uma virtualização reativa em Vue 3 usando Composition API e Pinia, com padrões…
Autenticação SSR em Nuxt 3 com refresh tokens rotativos e server routes
Guia prático para implementar autenticação SSR em Nuxt 3 usando refresh tokens rotativos, cookies httpOnly e server routes. Padrões, code samples e armadilhas…
Scroll infinito reativo no Vue 3 com Composition API e Pinia
Aprenda a implementar scroll infinito reativo em Vue 3 usando Composition API e Pinia: do setup ao mini-projeto, com exemplos práticos, store…
Autocomplete reativo em Vue 3 com debounce, cache local e Pinia
Tutorial prático para iniciantes: construa um autocomplete reativo em Vue 3 usando Composition API, debounce e cache local com Pinia. Mostro o jeito errado e…
Sincronizando estado entre abas com Pinia e BroadcastChannel (Composition API)
Aprenda a sincronizar estado entre abas usando Pinia e BroadcastChannel com Composition API. Guia passo a passo para iniciantes: setup, exemplos funcionais…