Skip to content

usePaginatedSetup / createPaginatedStore ​

Elimina el boilerplate de ~80 líneas que aparece en cada módulo con lista paginada.

Problema que resuelve ​

Antes de este composable, cada store paginado repetía:

ts
// ❌ Antes — ~80 líneas idénticas en CADA store
const items = ref<X[]>([])
const page = ref(1)
const limit = ref(20)
const total = ref(0)
const filters = ref({})
const sortBy = ref<string | undefined>(undefined)
const sortDir = ref<'asc' | 'desc'>('asc')
const lastCompanyId = ref<string | undefined>(undefined)
const isLoading = ref(false)
const error = ref<string | null>(null)

const totalPages = computed(...)
const hasPrev = computed(...)
const hasNext = computed(...)
// ... fetch(), goToPage(), setLimit(), applyFilters(), clearFilters(), setSort(), refresh()
// ... resetState() en cada cambio de empresa

Ahora:

ts
// ✅ Ahora — 4 líneas
const paginated = usePaginatedSetup<Branch, BranchFilters>({
  fetchFn: branchService.getBranches.bind(branchService),
  defaultFilters: { search: undefined, status: undefined },
})

API ​

Configuración ​

ts
interface PaginatedStoreConfig<T, F extends Record<string, unknown>> {
  fetchFn: (params: F & { page, limit, sortBy?, sortDir? }) => Promise<PagedResponse>
  defaultFilters: F
  defaultLimit?: number  // default: 20
}

WARNING

F (tipo de filtros) debe extender Record<string, unknown>:

ts
// ✅ Correcto
export interface BranchFilters extends Record<string, unknown> {
  search?: string
  status?: string
}

// ❌ Incorrecto — causa error TypeScript
export interface BranchFilters {
  search?: string
}

fetchFn — forma aceptada ​

El composable normaliza automáticamente dos formas de respuesta:

ts
// Forma A: respuesta directa del servicio (la más común en este proyecto)
async getInvoices(): Promise<InvoicesResponse>  // { success, data[], total, page, limit }

// Forma B: envuelta en axios
async getInvoices(): Promise<{ data: InvoicesResponse }>

// Ambas funcionan — el composable detecta cuál es cuál

Propiedades retornadas ​

PropiedadTipoDescripción
itemsRef<T[]>Array de la página actual
isLoadingRef<boolean>Estado de carga (list + CRUD)
errorRef<string|null>Último error
pageRef<number>Página actual
limitRef<number>Items por página
totalRef<number>Total de registros en el servidor
totalPagesComputedRef<number>Total de páginas
hasPrevComputedRef<boolean>¿Hay página anterior?
hasNextComputedRef<boolean>¿Hay página siguiente?
filtersRef<F>Estado de filtros activos
sortByRef<string|undefined>Columna de sort activa
sortDirRef<'asc'|'desc'>Dirección del sort

Métodos retornados ​

MétodoDescripción
fetch(targetPage?)Carga la página indicada (default: página actual)
goToPage(n)Navega a una página específica (clamped 1..totalPages)
setLimit(n)Cambia tamaño de página y recarga desde 1
applyFilters(partial)Fusiona filtros y vuelve a página 1
clearFilters()Resetea filtros a defaultFilters y recarga
setSort(sort)Cambia sortBy/sortDir y recarga desde 1
refresh()Recarga la página actual sin cambiar filtros ni sort
resetState()Limpia items, paginación, filtros y sort
withLoading(fn, msg?)Ejecuta fn con isLoading=true y captura errores

Cambio automático de empresa ​

El composable detecta cuando cambia la empresa activa y resetea el estado automáticamente:

ts
// Internamente:
if (companyId.value !== lastCompanyId.value) {
  resetState()
  lastCompanyId.value = companyId.value
}

No necesitas hacer nada adicional — al cambiar de empresa, la tabla vuelve a la página 1 con filtros limpios.

usePaginatedSetup vs createPaginatedStore ​

usePaginatedSetupcreatePaginatedStore
UsoDentro de defineStore()Genera el store completo
CRUDAgrega métodos propiosNo — solo lectura
CuándoMódulos con crear/editar/eliminarSolo listas de consulta
ts
// usePaginatedSetup — para módulos con CRUD
export const useBranchStore = defineStore('branch', () => {
  const paginated = usePaginatedSetup<Branch, BranchFilters>({ ... })
  const createBranch = async (payload) => paginated.withLoading(...)
  return { ...paginated, createBranch }
})

// createPaginatedStore — para listas de solo lectura
const useProductCatalog = createPaginatedStore<Product, ProductFilters>({
  id: 'product-catalog',
  fetchFn: catalogService.getProducts,
  defaultFilters: {},
})

Patrón completo de store CRUD ​

ts
// frontend/src/stores/invoice.store.ts
import { defineStore } from 'pinia'
import { invoiceService } from '@/services/invoice.service'
import { usePaginatedSetup } from '@/composables/createPaginatedStore'
import type { Invoice } from '@/types/invoice'

export interface InvoiceFilters extends Record<string, unknown> {
  search?: string
  status?: string
}

export const useInvoiceStore = defineStore('invoice', () => {
  const paginated = usePaginatedSetup<Invoice, InvoiceFilters>({
    fetchFn: invoiceService.getInvoices.bind(invoiceService),
    defaultFilters: { search: undefined, status: undefined },
  })

  // Alias de compatibilidad
  const invoices = paginated.items
  const fetchInvoices = (p?: number) => paginated.fetch(p)

  // CRUD — reutiliza el mismo isLoading/error de la lista
  const createInvoice = async (payload: CreateInvoicePayload) => {
    return paginated.withLoading(async () => {
      const res = await invoiceService.createInvoice(payload)
      await paginated.refresh()
      return res.data
    }, 'Error al crear la factura')
  }

  const updateInvoice = async (id: string, payload: UpdateInvoicePayload) => {
    await paginated.withLoading(async () => {
      await invoiceService.updateInvoice(id, payload)
      await paginated.refresh()
    }, 'Error al actualizar la factura')
  }

  const deleteInvoice = async (id: string) => {
    await paginated.withLoading(async () => {
      await invoiceService.deleteInvoice(id)
      await paginated.refresh()
    }, 'Error al eliminar la factura')
  }

  return {
    ...paginated,
    invoices,
    fetchInvoices,
    createInvoice,
    updateInvoice,
    deleteInvoice,
  }
})

Documentación de Kaleo — plataforma SaaS multi-tenant