100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Next.js App Router
55 minintermediate

Components Practice — Dashboard with Server Data

What You'll Build

You will build a Cricket Series Dashboard that combines every Module 2 concept into one working page: Server Components fetching data, parallel data fetching, a Server Action for a mutation, streaming with Suspense for a slow panel, and proper error and not-found handling. The dashboard shows series standings and a leading-scorer panel fetched on the server, a form that adds a note via a Server Action with revalidation, and a deliberately slow analytics panel that streams in behind a Suspense boundary while the rest of the page is usable immediately. By the end you will have a page that fetches securely on the server, mutates cleanly, streams gracefully, and fails safely — the complete server-and-client toolkit assembled into a realistic feature.

Analogy🏏Cricket
🏏 Think of it like cricket: Building this dashboard is like running a full match-day operation rather than a single drill — the scorers, the broadcast, the substitutes, and the ground staff all working together under live conditions. Just as match day proves every department functions in concert, this project proves server fetching, mutations, streaming, and error handling work together. Just as the broadcast keeps showing the ground while a replay loads, your dashboard stays usable while the slow panel streams. Just as the team has substitutes ready for injuries, your dashboard has error boundaries ready for failures. This is the match-day rehearsal that confirms the whole Module 2 system holds up under realistic load.

Prerequisites

  • A working App Router project from Module 1, or a fresh one scaffolded with create-next-app using the App Router.
  • Understanding of async Server Components and awaiting data directly during render.
  • Familiarity with Server Actions, the 'use server' directive, and revalidatePath.
  • Knowledge of Suspense boundaries and the loading.js convention for streaming.
  • Awareness of error.js, not-found.js, and the notFound function for graceful failures.

Setup & Project Structure

Plan the dashboard route before coding. You will create a dashboard route with a page that fetches standings and top scorer in parallel, an actions file holding the add-note Server Action, a slow analytics component wrapped in Suspense, and error and not-found files for safety. Laying out these files first makes each concept's home obvious and shows how server fetching, mutation, streaming, and error handling coexist in a single segment.

Analogy🏏Cricket
🏏 Think of it like cricket: a coach lays out the match-day plan before the toss — who gathers the standings and top-scorer stats, who handles a mid-innings change, which slow specialist analysis can arrive late, and what the fallback is if a player is injured — so every job has an owner before play. Just as planning each role first makes match day run smoothly, planning your dashboard folders first gives each concept a clear home: a page that fetches standings and top scorer in parallel, an actions file holding the add-note Server Action, a slow analytics component wrapped in Suspense, and error and not-found files for safety. Just as the plan shows how batting, bowling, and fielding combine, the layout shows how server fetching, mutation, streaming, and error handling fit together. The payoff: a structure where every piece has an obvious place before you write a line.
bash
# Target structure for the dashboard segment:
# app/
#   dashboard/
#     page.js          \u2190 parallel fetch + form + Suspense
#     actions.js       \u2190 'use server' addNote action
#     SlowAnalytics.js \u2190 deliberately slow, streamed component
#     loading.js       \u2190 route-level fallback skeleton
#     error.js         \u2190 client error boundary with reset
#     not-found.js     \u2190 friendly missing-data UI
#   lib/
#     series.js        \u2190 mock data sources

# Start the dev server
npm run dev   # http://localhost:3000/dashboard

Step 1 — Foundation

Step 1 builds the data layer and the parallel-fetching page. You create mock data functions that simulate network latency, then a Server Component page that fires the standings and top-scorer fetches concurrently with Promise.all so the page does not waterfall. This establishes the secure server-side data foundation everything else builds on, and demonstrates the parallel-fetch habit that keeps data-dense pages fast.

Analogy🏏Cricket
🏏 Think of it like cricket: Step 1 is like the analysts compiling the opposition report — multiple researchers working at once on batting, bowling, and fielding, then combining their findings into one briefing. Just as the researchers work concurrently rather than one after another, your fetches run in parallel with Promise.all. Just as the combined briefing is ready before the captain walks out, the data is resolved before the page renders. This shows why the foundation is parallel server fetching: gather everything at once, securely, before play begins.
javascript
// lib/series.js  \u2014  mock data sources with simulated latency
export async function getStandings() {
  await new Promise((r) => setTimeout(r, 300));
  return [
    { id: 1, name: 'India', points: 18 },
    { id: 2, name: 'Australia', points: 14 },
    { id: 3, name: 'England', points: 10 },
  ];
}
export async function getTopScorer() {
  await new Promise((r) => setTimeout(r, 300));
  return { name: 'Rohit Sharma', runs: 421 };
}

// app/dashboard/page.js  \u2014  parallel fetch in a Server Component
import { getStandings, getTopScorer } from '../../lib/series';

export default async function DashboardPage() {
  const [standings, topScorer] = await Promise.all([getStandings(), getTopScorer()]);
  return (
    <section>
      <h1>\ud83c\udfcf Series Dashboard</h1>
      <p>Leading scorer: {topScorer.name} ({topScorer.runs} runs)</p>
      <ol>{standings.map((t) => <li key={t.id}>{t.name}: {t.points} pts</li>)}</ol>
    </section>
  );
}

Step 2 — Core Logic

Step 2 adds the mutation: a Server Action that records a coach's note and revalidates the dashboard so the new note appears. The action validates input, mutates the store, and calls revalidatePath. The page renders a form bound directly to the action, requiring no client fetch code. This is the write half of the dashboard, demonstrating how Server Actions turn a form submission into a secure server mutation with automatic UI refresh.

Analogy🏏Cricket
🏏 Think of it like cricket: Step 2 is like the captain logging a tactical note in the official book mid-innings — a direct entry that updates the team's shared record everyone then sees. Just as the note goes straight into the authoritative book, the form submission goes straight to the Server Action. Just as the updated book is immediately visible to the staff, revalidatePath makes the new note immediately visible on the page. This shows why the action is the core write logic: one direct submission records and refreshes in a single motion.
javascript
// app/dashboard/actions.js
'use server';
import { revalidatePath } from 'next/cache';

const notes = [];   // imagine a real database

export async function addNote(formData) {
  const text = String(formData.get('note') ?? '').trim();
  if (text.length < 3) return { error: 'Note is too short' };
  notes.push({ id: crypto.randomUUID(), text });
  revalidatePath('/dashboard');   // refresh the page so the note shows
  return { ok: true };
}
export function getNotes() { return notes; }

// In app/dashboard/page.js, render the form and existing notes:
// import { addNote, getNotes } from './actions';
// <form action={addNote}>
//   <input name="note" placeholder="Coach's note" />
//   <button type="submit">Save note</button>
// </form>
// <ul>{getNotes().map((n) => <li key={n.id}>{n.text}</li>)}</ul>

Step 3 — Integration & Enhancement

Step 3 integrates streaming and safety. You add a deliberately slow analytics component wrapped in a Suspense boundary so the dashboard's fast content appears instantly while the analytics stream in, plus a loading.js skeleton, an error.js boundary with reset, and a not-found.js. This brings the read, write, streaming, and failure-handling pieces together so the dashboard is fast, mutable, and resilient all at once — the full Module 2 system operating as one feature.

Analogy🏏Cricket
🏏 Think of it like cricket: Step 3 is the live match with everything running together — the scoreboard updating, a replay streaming in behind a 'pending' graphic, and substitutes ready if a fielder goes down. Just as the replay loads without freezing the broadcast, the analytics stream without freezing the dashboard. Just as substitutes contain an injury to one position, the error boundary contains a failure to one segment. This reveals why integration is the payoff: the value is in fetching, mutating, streaming, and recovering all coexisting smoothly.
javascript
// app/dashboard/SlowAnalytics.js  \u2014  slow component to stream
export default async function SlowAnalytics() {
  await new Promise((r) => setTimeout(r, 2000));   // simulate slow service
  return <p>Net run rate leader: India (+1.42)</p>;
}

// app/dashboard/page.js  \u2014  wrap the slow part in Suspense
import { Suspense } from 'react';
import SlowAnalytics from './SlowAnalytics';
// inside the returned JSX, after the standings:
// <Suspense fallback={<p>Loading analytics\u2026</p>}>
//   <SlowAnalytics />
// </Suspense>

// app/dashboard/loading.js
export default function Loading() { return <p>Loading dashboard\u2026</p>; }

// app/dashboard/error.js
'use client';
export default function Error({ error, reset }) {
  return (<div><p>Dashboard failed to load.</p><button onClick={() => reset()}>Retry</button></div>);
}

// app/dashboard/not-found.js
export default function NotFound() { return <p>No series data available.</p>; }

Step 4 — Testing & Verification

Run the dashboard and verify each behaviour deliberately. Confirm the fast content paints before the analytics stream in, that adding a note refreshes the list without a reload, that a short note is rejected, and that the error boundary catches a thrown error. Verifying each path proves the concepts work together rather than just in isolation.

Analogy🏏Cricket
🏏 Think of it like cricket: a captain rehearses each match scenario in the nets before it counts — a quick single, a run-out call, a rejected review, an injury contingency — confirming each response works under real conditions rather than trusting it will. Just as rehearsing each scenario proves the plan holds, running your dashboard and verifying each behaviour deliberately proves the concepts work together: the fast content paints before the analytics stream in, adding a note refreshes the list without a reload, a too-short note is rejected, and the error boundary catches a thrown error. Just as testing both the smooth single and the failed review checks success and failure paths, you check both valid and invalid inputs. And just as net rehearsal turns separate skills into a coordinated performance, verifying each path proves the streaming, mutation, and error handling work in concert, not just in isolation. The payoff: confidence the whole dashboard behaves as designed.
bash
# With the dev server running, verify at /dashboard:
# 1. Page shows standings + top scorer immediately;
#    'Loading analytics\u2026' appears, then analytics stream in ~2s later.
# 2. Submit a valid note          \u2192 it appears in the list, no full reload.
# 3. Submit a 1-character note     \u2192 rejected with 'Note is too short'.
# 4. Temporarily throw in SlowAnalytics (throw new Error('boom'))
#                                  \u2192 error.js renders with a working Retry button.
# 5. Remove the throw; confirm the dashboard recovers via reset() without reload.

Warning: A common error is awaiting the two fetches on separate lines instead of with Promise.all, which silently turns parallel fetching into a slower waterfall. Another is forgetting revalidatePath in the action, which leaves the note list stale. Verify both: the page should load fast and the note should appear immediately after submit.

Extension Challenge: Add a second Suspense boundary around a separate slow component so two panels stream independently, give the analytics its own error.js by extracting it into a nested route segment, and add useTransition to a Client Component that calls addNote so the save button shows a pending state. This exercises granular streaming, nested error boundaries, and client-side action invocation together.

  • Server Components fetch data directly and securely; firing independent fetches with Promise.all avoids slow waterfalls.
  • A Server Action bound to a form mutates on the server and calls revalidatePath so the UI reflects the change without a reload.
  • Wrapping a slow component in Suspense lets fast dashboard content paint immediately while the slow panel streams in.
  • loading.js gives the whole route an automatic skeleton, while explicit Suspense controls streaming at finer granularity.
  • error.js isolates runtime failures to the segment and offers a reset to recover, while not-found.js handles missing data.
  • Validating action input and revalidating after mutation are the two habits that keep mutations secure and the UI consistent.
Lesson 12 of 35
0% complete