---
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-10-29T03:19:48.174Z"
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 the `"use client"` directive. Everything Review imports and renders (Avatar, FiveStarRating, the timestamp) comes along into the client bundle with it, so there's no need to mark those files too. Reviews and the homepage stay Server Components.

## 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/getting-started/server-and-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)
