Skip to content

Commit 9fc40f5

Browse files
authored
blog: react table (#331)
* blog: react table * update * update ERD
1 parent f333597 commit 9fc40f5

File tree

4 files changed

+349
-0
lines changed

4 files changed

+349
-0
lines changed

blog/react-table/cover.png

261 KB
Loading

blog/react-table/index.md

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
---
2+
title: "Rendering Prisma Queries With React Table: The Low-Code Way"
3+
description: This post introduces a low-code approach to rendering database rows loaded with Prisma ORM as a table component using React Table.
4+
tags: [prisma,react-table,react-query,zenstack]
5+
authors: yiming
6+
date: 2024-07-21
7+
image: ./cover.png
8+
---
9+
10+
# Rendering Prisma Queries With React Table: The Low-Code Way
11+
12+
![Cover Image](cover.png)
13+
14+
[React Table](https://tanstack.com/table), or more precisely, TanStack Table is a headless table UI library. If you're new to this kind of product, you'll probably ask, "What the heck is headless UI"? Isn't UI all about **the head**, after all? It all starts to make sense until you try something like React Table.
15+
16+
<!--truncate-->
17+
18+
For two reasons, tables are one of the nastiest things to build in web UI. First, they are difficult to render well, considering the amount of data they can hold, the interactions they allow, and the need to adapt to different screen sizes. Second, their state is complex: sorting, filtering, pagination, grouping, etc. React Table's philosophy is to solve the second problem well and leave the first entirely to you. It manages the state and logic of a table but doesn't touch the rendering part because:
19+
20+
> Building UI is a very branded and custom experience, even if that means choosing a design system or adhering to a design spec. - tanstack.com
21+
22+
Tables are most commonly used to render database query results — in modern times, the output of an ORM. In this post, I'll introduce a way of connecting [Prisma](https://prisma.io) - the most popular TypeScript ORM, to React Table, with the help of [React Query](https://tanstack.com/query) and [ZenStack](/). You'll be amazed by how little code you need to write to render a full-fledged table UI.
23+
24+
## A full-stack setup
25+
26+
We need a full-stack application to query a database and render the UI. In this example, I'll use Next.js as the framework, although the approach can be applied to other similar frameworks (like Nuxt, SvelteKit, etc.), or to an application with decoupled front-end and back-end.
27+
28+
We can easily create a new project with `npx create-next-app`. After that, we need to install several dependencies:
29+
30+
- Prisma - the ORM
31+
- ZenStack - a full-stack toolkit built above Prisma
32+
- React Query - the data-fetching library
33+
- React Table - the headless table library
34+
35+
We'll also use the legendary "North Wind" trading dataset (created by Microsoft many, many years ago) to feed our database. Here's its ERD:
36+
37+
```mermaid
38+
erDiagram
39+
Category {
40+
Int id PK
41+
String name
42+
String description
43+
}
44+
Customer {
45+
String id PK
46+
String name
47+
String contactName
48+
String contactTitle
49+
String address
50+
String city
51+
String postalCode
52+
String country
53+
}
54+
Employee {
55+
Int id PK
56+
String lastName
57+
String firstName
58+
String title
59+
String region
60+
}
61+
OrderDetail {
62+
Int orderId
63+
Int productId
64+
Float unitPrice
65+
Int quantity
66+
Float discount
67+
}
68+
Order {
69+
Int id PK
70+
String customerId
71+
Int employeeId
72+
DateTime orderDate
73+
DateTime shippedDate
74+
}
75+
Product {
76+
Int id PK
77+
String name
78+
Int categoryId
79+
Float unitPrice
80+
}
81+
Order ||--|{ OrderDetail : details
82+
Order }o--|| Customer : customer
83+
Order }o--|| Employee : employee
84+
OrderDetail }o--|| Product : product
85+
Product }o--|| Category : category
86+
87+
```
88+
89+
A Prisma schema file is authored to reflect this database structure.
90+
91+
## The "free lunch" API
92+
93+
SQL databases are not meant to be consumed from the frontend. You need an API to mediate. You can build such an API in many ways, but here we'll use [ZenStack](https://zenstack.dev) to "unbuild" it. ZenStack is a full-stack toolkit built above Prisma, and one of the cool things it does is to automagically derive a backend API from the schema.
94+
95+
Setting ZenStack up is very easy:
96+
97+
1. Run `npx zenstack init` to prep the project. It copies the `schema.prisma` file into `schema.zmodel` - which is the schema file used by ZenStack. ZModel is a superset of Prisma schema.
98+
99+
2. Whenever you make changes to `schema.zmodel`, run `npx zenstack generate` to regenerate the Prisma schema and `PrismaClient`.
100+
101+
ZenStack can provide a full set of CRUD API with Next.js in a few lines of code:
102+
103+
```ts title="src/app/api/model/[...path]/route.ts"
104+
import { prisma } from '@/server/db';
105+
import { NextRequestHandler } from '@zenstackhq/server/next';
106+
107+
const handler = NextRequestHandler({
108+
getPrisma: () => prisma,
109+
useAppDir: true,
110+
});
111+
112+
export {
113+
handler as DELETE,
114+
handler as GET,
115+
handler as PATCH,
116+
handler as POST,
117+
handler as PUT,
118+
};
119+
```
120+
121+
Now you have a set of APIs mounted at `/api/model` that mirrors `PrismaClient`:
122+
123+
- GET `/api/model/order/findMany?q=...`
124+
- GET `/api/model/order/count?q=...`
125+
- PUT `/api/model/order/update`
126+
- ...
127+
128+
The query parameters and body also follow the corresponding `PrismaClient` method parameters.
129+
130+
I know a big **🚨 NO THIS IS NOT SECURE 🚨** is flashing in your mind. Hold on, we'll get to that later.
131+
132+
## The "free lunch" hooks
133+
134+
Having a free API is cool, but writing `fetch` to call it is cumbersome. How about some free query hooks? Yes, add the `@zenstackhq/tanstack-query` plugin to the ZModel schema, and you'll have a set of fully typed React Query hooks generated for each model:
135+
136+
```ts title="schema.zmodel"
137+
plugin hooks {
138+
provider = '@zenstackhq/tanstack-query'
139+
target = 'react'
140+
output = 'src/hooks'
141+
}
142+
```
143+
144+
The hooks call into the APIs we installed in the previous section, and they also precisely mirror `PrismaClient`'s signature:
145+
146+
```ts
147+
import { useFindManyOrder } from '@/hooks/order';
148+
149+
// data is typed as `(Order & { details: OrderDetail[]; customer: Customer })[]`
150+
const { data, isLoading, error } = useFindManyOrder({
151+
where: { ... },
152+
orderBy: { ... },
153+
include: { details: true, customer: true }
154+
});
155+
```
156+
157+
Please note that although React Query and React Table are both from TanStack, you don't have to use them together. React Table is agnostic to the data fetching mechanism. They just happen to play very well together.
158+
159+
## Finally, let's build the table
160+
161+
Creating a basic table is straightforward, You define the columns and then initialize a table instance with data. We'll see how to do it by building a table to display order details.
162+
163+
```tsx
164+
// the relation fields included when querying `OrderDetail`
165+
const queryInclude = {
166+
include: {
167+
order: { include: { employee: true } },
168+
product: { include: { category: true } },
169+
},
170+
} satisfies Prisma.OrderDetailFindManyArgs;
171+
172+
// create a column helper to simplify the column definition
173+
// The `Prisma.OrderDetailGetPayload<typeof queryInclude>` type gives us
174+
// the shape of the query result
175+
const columnHelper =
176+
createColumnHelper<Prisma.OrderDetailGetPayload<typeof queryInclude>>();
177+
178+
const columns = [
179+
columnHelper.accessor('order.id', { header: () => <span>Order ID</span> }),
180+
181+
columnHelper.accessor('order.orderDate', {
182+
cell: (info) => info.getValue()?.toLocaleDateString(),
183+
header: () => <span>Date</span>,
184+
}),
185+
186+
// other columns ...
187+
188+
columnHelper.accessor('order.employee.firstName', {
189+
header: () => <span>Employee</span>,
190+
}),
191+
];
192+
193+
export const OrderDetails = () => {
194+
const { data } = useFindManyOrderDetail({
195+
...queryInclude,
196+
orderBy: computeOrderBy(),
197+
skip: pagination.pageIndex * pagination.pageSize,
198+
take: pagination.pageSize,
199+
});
200+
201+
const table = useReactTable({
202+
data: orders ?? [],
203+
columns,
204+
getCoreRowModel: getCoreRowModel(),
205+
});
206+
}
207+
```
208+
209+
We can then render the table with some basic TSX:
210+
211+
```tsx
212+
export const OrderDetails = () => {
213+
return (
214+
<table>
215+
<thead>
216+
{table.getHeaderGroups().map((headerGroup) => (
217+
<tr key={headerGroup.id}>
218+
{headerGroup.headers.map((header) => (
219+
<th key={header.id}>
220+
{flexRender(
221+
header.column.columnDef.header,
222+
header.getContext()
223+
)}
224+
</th>
225+
))}
226+
</tr>
227+
))}
228+
</thead>
229+
<tbody>
230+
{table.getRowModel().rows.map((row) => (
231+
<tr key={row.id}>
232+
{row.getVisibleCells().map((cell) => (
233+
<td key={cell.id}>
234+
{flexRender(
235+
cell.column.columnDef.cell,
236+
cell.getContext()
237+
)}
238+
</td>
239+
))}
240+
</tr>
241+
))}
242+
</tbody>
243+
);
244+
}
245+
```
246+
247+
With the help of column definitions, React Table knows how to fetch data for a cell and transform it as needed. You only need to focus on properly laying the table out.
248+
249+
![Simple table](table-1.png)
250+
251+
What's cool about React Table is that you don't need to flatten the nested query result into tabular form. The columns can be defined to reach into deeply nested objects.
252+
253+
## Making it fancier
254+
255+
Tables allow you to do many things besides viewing data. Let's use pagination as an example to demonstrate how to enable such interaction in our setup.
256+
257+
React Query has built-in support from front-end pagination. However, since we're rendering database tables, we want the pagination to run at the backend. First, we define a pagination state and set the table to use manual pagination mode (meaning that we handle the pagination ourselves):
258+
259+
```tsx
260+
261+
// highlight-start
262+
// pagination state
263+
const [pagination, setPagination] = useState<PaginationState>({
264+
pageIndex: 0,
265+
pageSize: PAGE_SIZE,
266+
});
267+
// highlight-end
268+
269+
// highlight-start
270+
// fetch total row count
271+
const { data: count } = useCountOrderDetail();
272+
// highlight-end
273+
274+
const table = useReactTable({
275+
...
276+
277+
// highlight-start
278+
// pagination
279+
manualPagination: true,
280+
onPaginationChange: setPagination,
281+
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
282+
283+
// state
284+
state: { agination },
285+
// highlight-end
286+
});
287+
```
288+
289+
Also, update the hooks call to respect the pagination state:
290+
291+
```tsx
292+
const { data } = useFindManyOrderDetail({
293+
...queryInclude,
294+
orderBy: computeOrderBy(),
295+
// highlight-start
296+
skip: pagination.pageIndex * pagination.pageSize,
297+
take: pagination.pageSize,
298+
// highlight-end
299+
});
300+
```
301+
302+
Finally, add navigation buttons:
303+
304+
```tsx
305+
<div>
306+
<button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
307+
Prev
308+
</button>
309+
<button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
310+
Next
311+
</button>
312+
<span className="ml-2">
313+
Page {table.getState().pagination.pageIndex + 1} of{' '}
314+
{table.getPageCount().toLocaleString()}
315+
</span>
316+
</div>
317+
```
318+
319+
This part well demonstrates the value of "headless" UI. You don't need to manage detailed pagination state anymore. Instead, provide the bare minimum logic and let React Table handle the rest. Sorting can be implemented similarly. Check out the link at the end of this post for the complete code.
320+
321+
![Table with pagination](table-2.png)
322+
323+
## Tons of flexibility
324+
325+
We've got a pretty cool table end-to-end working now, with roughly 200 lines of code. Less code is only one of the benefits of this combination. It also provides excellent flexibility in every layer of the stack:
326+
327+
- Prisma's query
328+
329+
Prisma is known for its concise yet powerful query API. It allows you to do complex joins and aggregations without writing SQL. In our example, our table shows data from five tables, and we barely noticed the complexity.
330+
331+
- ZenStack's access control
332+
333+
Remember I said we'll get back to the security issue? A real-world API must have an authorization mechanism with it. ZenStack's real power lies in its ability to define access control rules in the data schema. You can define rules like rejecting anonymous users or showing only the orders of the current login employee, etc. Read more details [here](/docs/the-complete-guide/part1/access-policy/).
334+
335+
- React Query's fetching
336+
337+
React Query provides great flexibility around how data is fetched, cached, and invalidated. Leverage its power to build a highly responsive UI and reduce the load on the database at same time.
338+
339+
- React Table's state management
340+
341+
React Table has every aspect of a table's state organized for you. It provides a solid pattern to follow without limiting how you render the table UI.
342+
343+
## Conclusion
344+
345+
The evolution of dev tools is like a pendulum swinging backward and forward between simplicity and flexibility. All these years of wisdom have distilled into awesome tools like React Table and React Query, which seem to have found a good balance. They are not the simplest to pick up, but they are simple enough yet wonderfully flexible.
346+
347+
---
348+
349+
The complete sample code: [https://github.com/ymc9/react-query-table-zenstack](https://github.com/ymc9/react-query-table-zenstack)

blog/react-table/table-1.png

490 KB
Loading

blog/react-table/table-2.png

495 KB
Loading

0 commit comments

Comments
 (0)