javascript
54 lines · 6 steps
A searchable, paginated table as a Next.js Server Component
An async Server Component reads URL params, queries Prisma, and renders a paginated, filterable invoice table.
Explained by
highlit
1import Link from "next/link";
2import { prisma } from "@/lib/prisma";
3
4const PAGE_SIZE = 20;
5
6export default async function InvoicesTable({ searchParams }) {
7 const params = await searchParams;
8 const query = (params.q ?? "").trim();
9 const page = Math.max(1, Number(params.page) || 1);
10
11 const where = query
12 ? { customer: { name: { contains: query, mode: "insensitive" } } }
13 : {};
14
15 const [invoices, total] = await Promise.all([
16 prisma.invoice.findMany({
17 where,
18 include: { customer: true },
19 orderBy: { issuedAt: "desc" },
20 skip: (page - 1) * PAGE_SIZE,
21 take: PAGE_SIZE,
22 }),
23 prisma.invoice.count({ where }),
24 ]);
25
26 const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
27 const hrefFor = (p) => `?${new URLSearchParams({ ...(query && { q: query }), page: String(p) })}`;
28
29 return (
30 <div>
31 <table>
32 <thead>
33 <tr><th>Invoice</th><th>Customer</th><th>Amount</th><th>Status</th></tr>
34 </thead>
35 <tbody>
36 {invoices.map((inv) => (
37 <tr key={inv.id}>
38 <td>{inv.number}</td>
39 <td>{inv.customer.name}</td>
40 <td>${(inv.amountCents / 100).toFixed(2)}</td>
41 <td>{inv.status}</td>
42 </tr>
43 ))}
44 </tbody>
45 </table>
46
47 <nav aria-label="Pagination">
48 {page > 1 && <Link href={hrefFor(page - 1)}>Previous</Link>}
49 <span>Page {page} of {totalPages}</span>
50 {page < totalPages && <Link href={hrefFor(page + 1)}>Next</Link>}
51 </nav>
52 </div>
53 );
54}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Async Server Components let you await data directly in the component body, skipping client-side fetching.
- 2Driving search and pagination from URL params makes state shareable, bookmarkable, and refresh-safe.
- 3Running the page query and count in parallel with Promise.all avoids serializing two round-trips.
Related explainers
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
javascript
import { useEffect, useRef, useState } from 'react'; export function useDelayedFlag(active, delay = 300) { const [visible, setVisible] = useState(false);
Delaying a loading spinner with a React hook
custom-hooks
debouncing
cleanup
Intermediate
8 steps
ruby
class SnippetHighlighter CONTEXT_RADIUS = 60 MAX_TERMS = 8
Building search-result snippets in Ruby
regular-expressions
text-processing
search
Intermediate
9 steps
javascript
const SWIPE_THRESHOLD = 80; const MAX_TRANSLATE = 120; export function attachSwipeToDismiss(element, onDismiss) {
Building a swipe-to-dismiss gesture in JS
touch-events
gesture-detection
dom-manipulation
Intermediate
10 steps
javascript
const { pool } = require('./db'); function withTransaction() { return async (req, res, next) => {
A per-request transaction middleware in Express
middleware
database-transactions
connection-pooling
Advanced
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/a-searchable-paginated-table-as-a-next-js-server-component-explained-javascript-12a0/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.