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

Walkthrough

Space play step click any line
Three takeaways
  1. 1Wrapping a slow async component in Suspense lets the page shell render immediately while data streams in behind a fallback.
  2. 2Keying Suspense on the query forces a fresh fallback whenever the search term changes, avoiding stale results.
  3. 3Server Components can await data directly, keeping fetching logic on the server and out of the client bundle.

Related explainers

go
func (h *ExportHandler) StreamExport(c *gin.Context) {
	datasetID := c.Param("id")
 
	ctx := c.Request.Context()

Streaming NDJSON progress with Gin

streaming goroutines channels
Advanced 8 steps
javascript
import { useState } from 'react';
 
function StarRating({ value = 0, max = 5, onChange, size = 24 }) {
  const [hovered, setHovered] = useState(null);

Building an accessible star rating in React

controlled-component accessibility state-management
Intermediate 8 steps
javascript
import { notFound } from 'next/navigation'
import { Suspense } from 'react'
import { getPostBySlug, getRelatedPosts } from '@/lib/posts'
import { RelatedPosts } from '@/components/related-posts'

Building a dynamic blog post page in Next.js

server-components dynamic-routing metadata
Intermediate 8 steps
javascript
const express = require('express');
const { createProxyMiddleware, fixRequestBody } = require('http-proxy-middleware');
 
const router = express.Router();

Building an API gateway proxy in Express

reverse-proxy middleware api-gateway
Intermediate 6 steps
javascript
import { useCallback, useRef, useState } from 'react';
 
export function ColorPicker({ initialColor = '#3b82f6', onCommit }) {
  const [committed, setCommitted] = useState(initialColor);

A validated color picker in React

uncontrolled-inputs refs validation
Intermediate 7 steps
javascript
import { useEffect, useRef } from 'react';
 
export function useRefetchOnFocus(refetch, { staleTime = 30_000 } = {}) {
  const lastFetchedAt = useRef(Date.now());

A React hook that refetches on tab focus

custom-hooks refs event-listeners
Intermediate 6 steps

Share this explainer

Here's the card — post it anywhere.

Streaming search results in a Next.js Server Component — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code