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
import { Suspense } from 'react'; import { getProducts } from '@/lib/api/products'; import { formatPrice } from '@/lib/format';
Streaming a product list with Suspense in Next.js
server-components
suspense
streaming
Intermediate
8 steps
java
package com.example.shop.repository; import com.example.shop.domain.Order; import com.example.shop.domain.OrderStatus;
Deriving JPA queries from method names in Spring
query derivation
repositories
pagination
Intermediate
8 steps
javascript
const DEFAULT_OPTIONS = { method: 'GET', timeout: 5000, retries: 3,
A resilient fetch wrapper with retries and timeout
fetch
retry
timeout
Intermediate
8 steps
javascript
import { parsePhoneNumberFromString } from 'libphonenumber-js'; export class PhoneValidationError extends Error { constructor(message, code) {
Normalizing phone numbers with typed errors
validation
error-handling
custom-errors
Intermediate
6 steps
javascript
const express = require('express'); const { z } = require('zod'); const router = express.Router();
Validating query params with Zod in Express
validation
schema-parsing
query-building
Intermediate
9 steps
javascript
const RESERVED_SLUGS = new Set(['new', 'edit', 'admin', 'api']); function slugify(title) { return title
Generating URL-safe unique slugs
string-normalization
regex
deduplication
Intermediate
9 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.