Paginated transactions table

using Tanstack Table and Tanstack Query.
This commit is contained in:
Luís Murta 2025-03-28 21:25:46 +00:00 committed by Luís Murta
parent 4743bed67f
commit f803f4b40d
5 changed files with 209 additions and 35 deletions

82
package-lock.json generated
View File

@ -8,6 +8,8 @@
"name": "webapp-spa-react",
"version": "0.0.0",
"dependencies": {
"@tanstack/react-query": "^5.71.0",
"@tanstack/react-table": "^8.21.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router": "^7.4.0"
@ -1321,6 +1323,65 @@
"win32"
]
},
"node_modules/@tanstack/query-core": {
"version": "5.71.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.71.0.tgz",
"integrity": "sha512-p4+T7CIEe1kMhii4booWiw42nuaiYI9La/bRCNzBaj1P3PDb0dEZYDhc/7oBifKJfHYN+mtS1ynW1qsmzQW7Og==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.71.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.71.0.tgz",
"integrity": "sha512-Udhlz9xHwk0iB7eLDchIqvu666NZFxPZZF80KnL8sZy+5J0kMvnJkzQNYRJwF70g8Vc1nn0TSMkPJgvx6+Pn4g==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.71.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^18 || ^19"
}
},
"node_modules/@tanstack/react-table": {
"version": "8.21.2",
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.2.tgz",
"integrity": "sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg==",
"license": "MIT",
"dependencies": {
"@tanstack/table-core": "8.21.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/@tanstack/table-core": {
"version": "8.21.2",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.2.tgz",
"integrity": "sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@ -3196,9 +3257,9 @@
}
},
"node_modules/vite": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.2.tgz",
"integrity": "sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==",
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz",
"integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3300,6 +3361,21 @@
"dev": true,
"license": "ISC"
},
"node_modules/yaml": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
"integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@ -10,6 +10,8 @@
"preview": "vite preview"
},
"dependencies": {
"@tanstack/react-query": "^5.71.0",
"@tanstack/react-table": "^8.21.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router": "^7.4.0"

View File

@ -3,9 +3,14 @@ import { createRoot } from "react-dom/client";
import { RouterProvider } from "react-router";
import "./index.css";
import { router } from "./routes.ts";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</StrictMode>
);

View File

@ -2,9 +2,7 @@ import { createBrowserRouter } from "react-router";
import App from "./root";
import Banks, { loader as banksLoader } from "./routes/banks";
import Categories, { loader as categoriesLoader } from "./routes/categories";
import Transactions, {
loader as transactionsLoader,
} from "./routes/transactions";
import Transactions from "./routes/transactions";
export const router = createBrowserRouter([
{
@ -14,7 +12,7 @@ export const router = createBrowserRouter([
{
path: "transactions",
Component: Transactions,
loader: transactionsLoader,
// loader: transactionsLoader, // loading transaction data is done on the component, as it need the pagination state
},
{
path: "banks",

View File

@ -1,37 +1,130 @@
import { useLoaderData } from "react-router";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import {
PaginationState,
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import { useState } from "react";
export async function loader() {
return await fetch(`http://localhost:9000/transactions`).then((response) => {
const PageSize = 30;
async function loader(page = 0) {
const limit = PageSize;
const offset = page * PageSize;
return await fetch(
`http://localhost:9000/transactions?limit=${limit}&offset=${offset}`
).then((response) => {
return response.json();
});
}
export default function Transactions() {
const data: {
type Transaction = {
id: number;
date: string;
description: string;
value: number;
}[] = useLoaderData<typeof loader>();
};
const columnHelper = createColumnHelper<Transaction>();
const columns = [
columnHelper.accessor("date", {}),
columnHelper.accessor("description", {}),
columnHelper.accessor("value", {}),
];
export default function Transactions() {
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: PageSize,
});
// const data: Transaction[] = useLoaderData<typeof loader>();
const { isPending, data } = useQuery({
queryKey: ["transactions", pagination.pageIndex],
queryFn: () => loader(pagination.pageIndex),
placeholderData: keepPreviousData,
});
const table = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
// getPaginationRowModel: getPaginationRowModel(), // not needed for server-side pagination
manualPagination: true,
// rowCount: , // TODO: get this from the server
pageCount: -1,
onPaginationChange: setPagination,
state: {
pagination,
},
});
if (isPending) {
return <div>Loading...</div>;
}
return (
<>
<table>
<thead>
<tr>
<th scope="col">date</th>
<th scope="col">description</th>
<th scope="col">value</th>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{data.map((t) => (
<tr>
<th>{t.date}</th>
<td>{t.description}</td>
<td>{t.value}</td>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
<div>
<button
onClick={() => table.firstPage()}
disabled={!table.getCanPreviousPage()}
>
First
</button>
<button
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</button>
<button
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</button>
<button
onClick={() => table.lastPage()}
disabled={!table.getCanNextPage()}
>
Last
</button>
</div>
<div>Page {table.getState().pagination.pageIndex + 1}</div>
</>
);
}