Paginated transactions table
using Tanstack Table and Tanstack Query.
This commit is contained in:
parent
4743bed67f
commit
f803f4b40d
82
package-lock.json
generated
82
package-lock.json
generated
@ -8,6 +8,8 @@
|
|||||||
"name": "webapp-spa-react",
|
"name": "webapp-spa-react",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tanstack/react-query": "^5.71.0",
|
||||||
|
"@tanstack/react-table": "^8.21.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router": "^7.4.0"
|
"react-router": "^7.4.0"
|
||||||
@ -1321,6 +1323,65 @@
|
|||||||
"win32"
|
"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": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@ -3196,9 +3257,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.2.2",
|
"version": "6.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz",
|
||||||
"integrity": "sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==",
|
"integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -3300,6 +3361,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/yocto-queue": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
|
|||||||
@ -10,6 +10,8 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tanstack/react-query": "^5.71.0",
|
||||||
|
"@tanstack/react-table": "^8.21.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router": "^7.4.0"
|
"react-router": "^7.4.0"
|
||||||
|
|||||||
@ -3,9 +3,14 @@ import { createRoot } from "react-dom/client";
|
|||||||
import { RouterProvider } from "react-router";
|
import { RouterProvider } from "react-router";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { router } from "./routes.ts";
|
import { router } from "./routes.ts";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<RouterProvider router={router} />
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</QueryClientProvider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,9 +2,7 @@ import { createBrowserRouter } from "react-router";
|
|||||||
import App from "./root";
|
import App from "./root";
|
||||||
import Banks, { loader as banksLoader } from "./routes/banks";
|
import Banks, { loader as banksLoader } from "./routes/banks";
|
||||||
import Categories, { loader as categoriesLoader } from "./routes/categories";
|
import Categories, { loader as categoriesLoader } from "./routes/categories";
|
||||||
import Transactions, {
|
import Transactions from "./routes/transactions";
|
||||||
loader as transactionsLoader,
|
|
||||||
} from "./routes/transactions";
|
|
||||||
|
|
||||||
export const router = createBrowserRouter([
|
export const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -14,7 +12,7 @@ export const router = createBrowserRouter([
|
|||||||
{
|
{
|
||||||
path: "transactions",
|
path: "transactions",
|
||||||
Component: Transactions,
|
Component: Transactions,
|
||||||
loader: transactionsLoader,
|
// loader: transactionsLoader, // loading transaction data is done on the component, as it need the pagination state
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "banks",
|
path: "banks",
|
||||||
|
|||||||
@ -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() {
|
const PageSize = 30;
|
||||||
return await fetch(`http://localhost:9000/transactions`).then((response) => {
|
|
||||||
|
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();
|
return response.json();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Transaction = {
|
||||||
|
id: number;
|
||||||
|
date: string;
|
||||||
|
description: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<Transaction>();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
columnHelper.accessor("date", {}),
|
||||||
|
columnHelper.accessor("description", {}),
|
||||||
|
columnHelper.accessor("value", {}),
|
||||||
|
];
|
||||||
|
|
||||||
export default function Transactions() {
|
export default function Transactions() {
|
||||||
const data: {
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
id: number;
|
pageIndex: 0,
|
||||||
date: string;
|
pageSize: PageSize,
|
||||||
description: string;
|
});
|
||||||
value: number;
|
|
||||||
}[] = useLoaderData<typeof loader>();
|
// 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 (
|
return (
|
||||||
<table>
|
<>
|
||||||
<thead>
|
<table>
|
||||||
<tr>
|
<thead>
|
||||||
<th scope="col">date</th>
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<th scope="col">description</th>
|
<tr key={headerGroup.id}>
|
||||||
<th scope="col">value</th>
|
{headerGroup.headers.map((header) => (
|
||||||
</tr>
|
<th key={header.id}>
|
||||||
</thead>
|
{header.isPlaceholder
|
||||||
<tbody>
|
? null
|
||||||
{data.map((t) => (
|
: flexRender(
|
||||||
<tr>
|
header.column.columnDef.header,
|
||||||
<th>{t.date}</th>
|
header.getContext()
|
||||||
<td>{t.description}</td>
|
)}
|
||||||
<td>{t.value}</td>
|
</th>
|
||||||
</tr>
|
))}
|
||||||
))}
|
</tr>
|
||||||
</tbody>
|
))}
|
||||||
</table>
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user