javascript
52 lines · 8 steps
Streaming search results in a Next.js Server Component
A server-rendered search page that streams results independently with Suspense while keeping the input instantly interactive.
Explained by
highlit
1import { Suspense } from 'react';
2import { searchProducts } from '@/lib/products';
3import SearchInput from './search-input';
4
5export const dynamic = 'force-dynamic';
6
7export default async function ProductSearchPage({ searchParams }) {
8 const { q = '' } = await searchParams;
9
10 return (
11 <section className="mx-auto max-w-2xl p-6">
12 <h1 className="mb-4 text-xl font-semibold">Search products</h1>
13 <SearchInput defaultValue={q} />
14 <Suspense key={q} fallback={<ResultsSkeleton />}>
15 <SearchResults query={q} />
16 </Suspense>
17 </section>
18 );
19}
20
21async function SearchResults({ query }) {
22 const term = query.trim();
23 if (!term) {
24 return <p className="mt-6 text-sm text-gray-500">Start typing to search.</p>;
25 }
26
27 const products = await searchProducts(term);
28 if (products.length === 0) {
29 return <p className="mt-6 text-sm text-gray-500">No matches for “{term}”.</p>;
30 }
31
32 return (
33 <ul className="mt-6 divide-y">
34 {products.map((product) => (
35 <li key={product.id} className="flex justify-between py-3">
36 <span>{product.name}</span>
37 <span className="text-gray-500">${product.price.toFixed(2)}</span>
38 </li>
39 ))}
40 </ul>
41 );
42}
43
44function ResultsSkeleton() {
45 return (
46 <ul className="mt-6 animate-pulse space-y-3">
47 {Array.from({ length: 5 }).map((_, i) => (
48 <li key={i} className="h-6 rounded bg-gray-200" />
49 ))}
50 </ul>
51 );
52}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Wrapping a slow async component in Suspense lets the page shell render immediately while data streams in behind a fallback.
- 2Keying Suspense on the query forces a fresh fallback whenever the search term changes, avoiding stale results.
- 3Server Components can await data directly, keeping fetching logic on the server and out of the client bundle.
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
python
import random from typing import Iterator, List
How reservoir sampling picks k items
reservoir-sampling
streaming
randomness
Intermediate
5 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
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/streaming-search-results-in-a-next-js-server-component-explained-javascript-996d/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.