Vercel Logo

Review display components

Good UI components are reusable, type-safe, and handle edge cases. You'll build components that work across your app and gracefully handle missing data like avatars or malformed dates.

Outcome

Create Review and FiveStarRating components that display customer reviews with stars, avatars, timestamps, and proper styling.

Fast Track

  1. Run pnpm add ms && pnpm add -D @types/ms, create components/five-star-rating.tsx using lucide-react Star icons
  2. Create components/review.tsx as a Client Component with Avatar, FiveStarRating, and relative time using ms
  3. Create components/reviews.tsx to map reviews with Separators, wrap product cards in Links on homepage

Hands-on Exercise 1.3

Build UI components for displaying product reviews:

Requirements:

  1. Create a FiveStarRating component that displays 1-5 filled stars
  2. Create a Review component that shows reviewer avatar, name, rating, date, and review text
  3. Format dates as relative time ("2 days ago", "3 weeks ago")
  4. Use shadcn/ui Avatar component with fallback initials
  5. Make all components type-safe with the Review type from Lesson 1.2

Implementation hints:

  • Star rating: Use lucide-react icons (Star, StarHalf)
  • Timestamps: Install and use the ms library for relative time
  • Avatars: Extract initials from reviewer name for fallback
  • The Review component should be a Client Component (uses Date.now())
  • Use Separator component between reviews

Step 1: Install Dependencies

pnpm add ms
pnpm add -D @types/ms

The ms library converts milliseconds to human-readable strings.

Step 2: Create FiveStarRating Component

Create components/five-star-rating.tsx:

components/five-star-rating.tsx
import { Star } from "lucide-react";
 
export function FiveStarRating({ rating }: { rating: number }) {
  return (
    <div className="flex gap-0.5">
      {Array.from({ length: 5 }).map((_, i) => (
        <Star
          key={i}
          className={`h-4 w-4 ${
            i < rating
              ? "fill-yellow-400 text-yellow-400"
              : "fill-gray-200 text-gray-200"
          }`}
        />
      ))}
    </div>
  );
}

What this does:

  • Creates 5 star icons
  • Fills stars based on rating (1-5)
  • Uses Tailwind for colors (yellow for filled, gray for empty)

Step 3: Create Review Component

Create components/review.tsx:

components/review.tsx
"use client";
 
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Review as ReviewType } from "@/lib/types";
import ms from "ms";
import { FiveStarRating } from "./five-star-rating";
 
export function Review({ review }: { review: ReviewType }) {
  const date = new Date(review.date);
 
  return (
    <div className="flex gap-4">
      <Avatar>
        <AvatarFallback>{getInitials(review.reviewer)}</AvatarFallback>
      </Avatar>
 
      <div className="flex-1 space-y-2">
        <div className="flex items-center justify-between">
          <div>
            <p className="font-medium text-sm">{review.reviewer}</p>
            <div className="flex items-center gap-2 mt-1">
              <FiveStarRating rating={review.stars} />
              <time className="text-xs text-muted-foreground" suppressHydrationWarning>
                {timeAgo(date)}
              </time>
            </div>
          </div>
        </div>
 
        <p className="text-sm leading-relaxed text-muted-foreground">
          {review.review}
        </p>
      </div>
    </div>
  );
}
 
function getInitials(name: string): string {
  return name
    .split(" ")
    .map((word) => word[0])
    .join("")
    .toUpperCase()
    .slice(0, 2);
}
 
function timeAgo(date: Date, suffix = true): string {
  const now = Date.now();
  const diff = now - date.getTime();
 
  if (diff < 1000) {
    return "Just now";
  }
 
  return `${ms(diff, { long: true })}${suffix ? " ago" : ""}`;
}

Key features:

  • "use client" directive (needed for Date.now() and suppressHydrationWarning)
  • Avatar with fallback initials
  • Five-star rating display
  • Relative timestamp ("2 days ago")
  • Flexible layout with Flexbox

Why suppressHydrationWarning? Server-rendered timestamps differ from client-rendered ones (server time vs client time). This prop tells React to expect mismatches on first render.

Step 4: Create Reviews Container Component

Create components/reviews.tsx:

components/reviews.tsx
import { Product } from "@/lib/types";
import { Review } from "./review";
import { Separator } from "./ui/separator";
 
export function Reviews({ product }: { product: Product }) {
  return (
    <div className="space-y-6">
      <h2 className="text-2xl font-bold">Customer Reviews</h2>
 
      <div className="space-y-6">
        {product.reviews.map((review, index) => (
          <div key={index}>
            <Review review={review} />
            {index < product.reviews.length - 1 && (
              <Separator className="mt-6" />
            )}
          </div>
        ))}
      </div>
    </div>
  );
}

What this does:

  • Maps over product reviews
  • Renders Review component for each
  • Adds Separator between reviews (but not after the last one)

Step 5: Update Homepage

Update app/page.tsx to display star ratings and link to individual products:

app/page.tsx
import Link from "next/link";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { FiveStarRating } from "@/components/five-star-rating";
import { getProducts } from "@/lib/sample-data";
 
export default function Home() {
  const products = getProducts();
 
  function averageRating(reviews: { stars: number }[]) {
    if (reviews.length === 0) return 0;
    return reviews.reduce((sum, r) => sum + r.stars, 0) / reviews.length;
  }
 
  return (
    <main className="min-h-screen p-8">
      <div className="max-w-4xl mx-auto space-y-8">
        <h1 className="text-4xl font-bold">Product Reviews</h1>
 
        <div className="grid gap-4">
          {products.map((product) => (
            <Link key={product.slug} href={`/${product.slug}`}>
              <Card className="hover:border-primary transition-colors cursor-pointer">
                <CardHeader>
                  <CardTitle>{product.name}</CardTitle>
                  <div className="flex items-center gap-2">
                    <FiveStarRating rating={Math.round(averageRating(product.reviews))} />
                    <span className="text-sm text-muted-foreground">
                      {product.reviews.length} reviews
                    </span>
                  </div>
                </CardHeader>
                <CardContent>
                  <p className="text-sm text-muted-foreground">
                    {product.description}
                  </p>
                </CardContent>
              </Card>
            </Link>
          ))}
        </div>
      </div>
    </main>
  );
}

Changes:

  • Imported FiveStarRating component
  • Added averageRating helper function
  • Display star rating with review count in each card
  • Wrapped cards in Link component with hover effect
  • Links to /{product.slug} (we'll create these pages in the next lesson)

Try It

  1. Visit the homepage at http://localhost:3000

  2. You should see:

    • 3 product cards with names and descriptions
    • Star ratings showing average rating for each product
    • Review count next to the stars
    • Hover effect on cards (border color change)
  3. Click a product card — it will 404 for now (we'll create product pages in Lesson 1.4)

Understanding Client Components

Why is Review a Client Component?

The timeAgo function uses Date.now(), which is a dynamic value that changes every millisecond. Next.js can't statically generate or cache this because it's time-dependent.

"use client"; // Required because of Date.now()
 
function timeAgo(date: Date): string {
  const diff = Date.now() - date.getTime(); // Date.now() changes constantly
  // ...
}

Server vs Client Components:

FeatureServer ComponentClient Component
Can use Date.now()❌ No✅ Yes
Can use React hooks❌ No✅ Yes
Can use event handlers❌ No✅ Yes
Bundle sent to client❌ No (smaller)✅ Yes (larger)
Default in Next.js 16✅ YesOpt-in with "use client"

Best practice: Use Server Components by default, Client Components only when needed.

Component Architecture

Your component tree:

Reviews (Server Component)
  └── Review (Client Component) ← Uses Date.now()
        ├── Avatar
        ├── FiveStarRating
        └── Timestamp

Only the Review component needs to be a Client Component. Everything else stays as Server Components for better performance.

Done-When

  • FiveStarRating component displays 1-5 stars
  • Review component shows avatar, name, rating, date, and text
  • Timestamps format as relative time ("2 days ago")
  • Components are fully type-safe
  • Homepage shows star ratings with average for each product
  • Homepage links to product pages (ready for next lesson)

What's Next

Your review UI is built and ready to display. In the next lesson, you'll create dynamic routes for individual product pages using Next.js App Router. You'll use generateStaticParams to pre-render all product pages at build time.


Sources: