---
title: "Review Display Components"
description: "Create reusable React components for displaying product reviews. Build a five-star rating component, format timestamps as relative time, and display reviewer avatars with fallbacks."
canonical_url: "https://vercel.com/academy/ai-summary-app-with-nextjs/review-display-components"
md_url: "https://vercel.com/academy/ai-summary-app-with-nextjs/review-display-components.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-04-11T11:43:53.272Z"
content_type: "lesson"
course: "ai-summary-app-with-nextjs"
course_title: "Creating an AI Summary App with Next.js"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Review Display Components

# 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

```bash
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`:

```tsx title="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`:

```tsx title="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`:

```tsx title="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:

```tsx title="app/page.tsx" {1,3,8-12,24-27}
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.

```tsx
"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:**

| Feature                | Server Component | Client 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  | ✅ Yes            | Opt-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:**

- [Next.js Client Components](https://nextjs.org/docs/app/building-your-application/rendering/client-components)
- [shadcn/ui Avatar](https://ui.shadcn.com/docs/components/avatar)
- [lucide-react Icons](https://lucide.dev)
- [ms library](https://github.com/vercel/ms)


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
