useTableView
Configura una tabla TanStack paginada con sort server-side en una sola línea.
Problema que resuelve
Este bloque de ~14 líneas se repetía en cada vista con tabla paginada:
ts
// ❌ Antes — 14 líneas idénticas en MembersView y BranchesView
const memberSorting = useSortingState()
const memberTable = useAppTable<Member>({
columns: useMemberColumns(),
data: computed(() => memberStore.members),
manualSorting: true,
manualPagination: true,
autoResetPageIndex: false,
...useControlledSorting(memberSorting, (sorting) => {
memberStore.setSort(sortingStateToServer(sorting)).catch(() => {})
}),
})
const isInitialLoading = computed(
() => memberStore.isLoading && memberStore.members.length === 0
)Con useTableView:
ts
// ✅ Ahora — 1 línea
const { table, isInitialLoading } = useTableView(useMemberColumns(), memberStore)Requisito del store
El store debe exponer estas 3 propiedades (cualquier store creado con usePaginatedSetup las tiene automáticamente):
ts
interface TableViewStore<T> {
items: T[] // Array reactivo de la página actual
isLoading: boolean // Estado de carga
setSort: (sort: ServerSort) => Promise<void> // Aplica sort server-side
}API
ts
function useTableView<T extends RowData>(
columns: AppColumnDef<T>[],
store: TableViewStore<T>,
): {
table: AppVueTable<AppFeatures, T, {}, {}, {}>
isInitialLoading: ComputedRef<boolean>
}isInitialLoading
ts
// true solo cuando: loading=true Y no hay datos previos
const isInitialLoading = computed(
() => store.isLoading && store.items.length === 0
)Esto significa:
- Primera carga:
isInitialLoading=true→ muestra el skeleton - Re-fetch (cambio de filtro/página):
isInitialLoading=false→ la tabla existente sigue visible mientras recarga
Por qué data debe ser computed
TanStack Table solo reacciona a refs o computed. Si se pasa el array directamente (que Pinia desenvuelve), la tabla "congela" los datos del mount:
ts
// ❌ Incorrecto — Pinia desenvuelve el ref → array plano → tabla no reacciona
data: store.items
// ✅ Correcto — useTableView lo hace automáticamente
data: computed(() => store.items)WARNING
Si ves que la tabla no actualiza al cambiar de página o filtrar, este es el bug más probable. useTableView lo previene internamente.
Los filtros son separados
useTableView maneja sort y datos, pero no filtros. Los filtros siguen siendo responsabilidad de la view via useFilterSync. Esta separación permite que:
- La view decida qué filtros mostrar y cómo mapearlos
- El sort y la tabla estén siempre configurados correctamente sin código repetido
ts
// View típica con useTableView + useFilterSync
const { table, isInitialLoading } = useTableView(useInvoiceColumns(), invoiceStore)
const filterValues = ref({ search: '', status: '' })
useFilterSync(filterValues, v => invoiceStore.applyFilters({
search: v.search || undefined,
status: v.status || undefined,
}))Ejemplo completo en una view
vue
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { useInvoiceStore } from '@/stores/invoice.store'
import { useInvoiceColumns } from '@/composables/useInvoiceColumns'
import { useTableView } from '@/composables/useTableView'
import { useFilterSync } from '@/composables/useFilterSync'
import { useCompanyPath } from '@/composables/useCompanyPath'
const invoiceStore = useInvoiceStore()
const { companyId } = useCompanyPath()
// 1 línea para configurar toda la tabla con sort server-side
const { table, isInitialLoading } = useTableView(useInvoiceColumns(), invoiceStore)
// Filtros por separado
const filterValues = ref({ search: '', status: '' })
watch(companyId, () => { filterValues.value = { search: '', status: '' } })
useFilterSync(filterValues, v => invoiceStore.applyFilters({ search: v.search || undefined }))
onMounted(() => invoiceStore.fetchInvoices())
</script>
<template>
<InvoiceTable
:table="table"
:loading="isInitialLoading"
:error="invoiceStore.error"
@retry="invoiceStore.fetchInvoices()"
/>
<UiPagination
:page="invoiceStore.page"
:total="invoiceStore.total"
:limit="invoiceStore.limit"
@update:page="invoiceStore.goToPage"
@update:limit="invoiceStore.setLimit"
/>
</template>