App Integration: Fetching and Displaying Tasks¶
Now, let's put these concepts into practice by refactoring our Task Management App. We will:
- Create a mock API route to simulate fetching tasks.
- Update our
app/dashboard/page.tsx(a Server Component) to fetch data from this mock API. - Modify our
components/TaskList.tsxto receive tasks as a prop, as it will no longer be responsible for fetching its own initial data. - Implement a loading UI for our dashboard.
1. Create a Mock API Route (app/api/tasks/route.ts)¶
Let's create a simple API endpoint that will return a hardcoded list of tasks. In a real application, this would connect to a database.
-
Create new folders and a file for your API route:
my-task-app/ └── app/ ├── ... └── api/ <-- NEW DIRECTORY └── tasks/ <-- NEW DIRECTORY └── route.ts <-- NEW FILE -
Add the following content to
app/api/tasks/route.ts. This file defines a Route Handler, which is equivalent to creating an API endpoint.app/api/tasks/route.ts// app/api/tasks/route.ts // This file defines a Next.js Route Handler for the /api/tasks endpoint. // It simulates fetching tasks from a backend. import { NextResponse } from 'next/server'; // Define a type for our mock task data interface MockTask { id: string; title: string; description: string; completed: boolean; priority: 'Low' | 'Medium' | 'High'; dueDate: string; // YYYY-MM-DD format } // Simulate a delay to demonstrate loading states const simulateDelay = (ms: number) => new Promise(res => setTimeout(res, ms)); // Hardcoded mock task data const mockTasks: MockTask[] = [ { id: 'task-1', title: 'Complete Module 5 of Next.js Course', description: 'Understand Server Components, SSR, SSG, and implement data fetching.', completed: false, priority: 'High', dueDate: '2025-06-15', }, { id: 'task-2', title: 'Refactor TaskList to accept props', description: 'Remove internal state for initial task loading and pass tasks via props.', completed: false, priority: 'High', dueDate: '2025-06-12', }, { id: 'task-3', title: 'Set up Next.js authentication', description: 'Prepare for integrating Firebase Authentication in Module 8.', completed: false, priority: 'Medium', dueDate: '2025-06-20', }, { id: 'task-4', title: 'Buy groceries', description: 'Milk, eggs, bread, vegetables, fruits.', completed: true, priority: 'Low', dueDate: '2025-06-05', }, ]; // GET handler for fetching tasks export async function GET() { console.log('Fetching tasks from mock API...'); await simulateDelay(1500); // Simulate network latency return NextResponse.json(mockTasks); }Key points about Route Handlers:
- They live in the
app/apidirectory. - The file name
route.ts(orroute.js) is special and defines the handler for the route segment. - Exported functions like
GET,POST,PUT,DELETEhandle specific HTTP methods. NextResponse.json()is used to return JSON data.- We added a
simulateDelayto mimic real-world network requests, which will be useful for testing our loading UI.
- They live in the
2. Update app/dashboard/page.tsx (Server Component for Data Fetching)¶
Now, we'll modify our dashboard page to fetch tasks from the mock API on the server and then pass them down to the TaskList component.
app/dashboard/page.tsx (Update this file)
// app/dashboard/page.tsx
// This is the dashboard page, now a Server Component that fetches tasks.
// It will fetch data and pass it to the TaskList Client Component.
import TaskList from '../../components/TaskList'; // Import TaskList
import { Metadata } from 'next'; // For page-specific metadata
// Define the type for our fetched tasks (should match MockTask from route.ts)
interface FetchedTask {
id: string;
title: string;
description: string;
completed: boolean;
priority: 'Low' | 'Medium' | 'High';
dueDate: string;
}
// Metadata for this specific page (optional, but good practice for SEO)
export const metadata: Metadata = {
title: 'Dashboard | My TaskFlow',
description: 'View and manage all your tasks from your personalized dashboard.',
};
// This is an async Server Component.
// It will fetch data before rendering the page.
export default async function DashboardPage() {
let tasks: FetchedTask[] = [];
let error: string | null = null;
try {
// Fetch data from our mock API endpoint
// Next.js automatically caches fetch requests, which is great for performance.
// For this demonstration, we'll assume default caching for now.
const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/api/tasks`, {
cache: 'no-store', // This ensures data is always fresh on every request (SSR behavior)
});
if (!response.ok) {
throw new Error(`Failed to fetch tasks: ${response.statusText}`);
}
tasks = await response.json();
} catch (err) {
error = err instanceof Error ? err.message : 'An unknown error occurred.';
console.error('Error fetching tasks:', error);
}
return (
<div className="flex flex-col items-center p-4">
<h2 className="text-3xl font-bold text-gray-800 mb-6">Your Dashboard</h2>
<p className="text-lg text-gray-600 mb-8">
Manage your tasks efficiently right from here.
</p>
{error ? (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
<strong className="font-bold">Error!</strong>
<span className="block sm:inline"> {error}</span>
</div>
) : (
// Pass the fetched tasks as props to the TaskList Client Component
<TaskList initialTasks={tasks} />
)}
</div>
);
}
Important Notes:
- We've made
DashboardPageanasyncfunction, allowing it to useawaitforfetch. - The
fetchcall is made directly inside the Server Component. process.env.NEXT_PUBLIC_BASE_URLis used for the API URL. You might need to addNEXT_PUBLIC_BASE_URL=http://localhost:3000to your.env.localfile for development.- We've added
cache: 'no-store'to thefetchoptions. This explicitly tells Next.js not to cache the response for this specific fetch call, ensuring that the tasks are always fetched fresh on every request. This behavior makes the component effectively Server-Side Rendered (SSR) for this data. Withoutno-store, Next.js might default to caching (SSG-like behavior), which is not what we want for frequently changing user tasks. - Error handling for the fetch request is included.
3. Modify components/TaskList.tsx (Pure UI Component)¶
Now that app/dashboard/page.tsx is responsible for fetching the initial tasks, our TaskList component no longer needs its own useState for tasks or the useEffect for initial loading. It will simply receive the initialTasks as a prop. The addTask, toggleTaskCompletion, and deleteTask functions will still operate on the internal tasks state for now, but in Module 6 (Server Actions), these will be refactored to interact with the server.
components/TaskList.tsx (Update this file)
// components/TaskList.tsx
// This Client Component now receives initial tasks as props from a Server Component.
// Its state management for tasks (add, toggle, delete) remains client-side for now,
// but will be refactored with Server Actions in a future module.
'use client';
import { useState, useEffect, FormEvent } from 'react';
// Re-use the Task interface definition for consistency
interface Task {
id: string;
title: string;
description: string; // Added description based on mock data
completed: boolean;
priority: 'Low' | 'Medium' | 'High'; // Added priority
dueDate: string; // Added dueDate
}
// Define props for TaskList component
interface TaskListProps {
initialTasks: Task[];
}
export default function TaskList({ initialTasks }: TaskListProps) {
// Initialize tasks state with the `initialTasks` passed as prop
const [tasks, setTasks] = useState<Task[]>(initialTasks);
const [newTaskTitle, setNewTaskTitle] = useState<string>('');
// Update internal state if initialTasks prop changes (e.g., re-fetching from server)
useEffect(() => {
setTasks(initialTasks);
}, [initialTasks]);
// Function to add a new task (client-side only for now)
const addTask = (event: FormEvent) => {
event.preventDefault();
if (newTaskTitle.trim() === '') {
console.log('Task title cannot be empty.');
// In a real app, you would show a user-friendly error message (e.g., a toast or modal)
return;
}
const newTask: Task = {
id: crypto.randomUUID(), // Use a more robust unique ID
title: newTaskTitle,
description: 'No description provided.', // Default description
completed: false,
priority: 'Medium', // Default priority
dueDate: new Date().toISOString().split('T')[0], // Default to today's date
};
setTasks([...tasks, newTask]);
setNewTaskTitle('');
};
// Function to toggle task completion status (client-side only for now)
const toggleTaskCompletion = (id: string) => {
setTasks(
tasks.map((task) =>
task.id === id ? { ...task, completed: !task.completed } : task
)
);
};
// Function to delete a task (client-side only for now)
const deleteTask = (id: string) => {
setTasks(tasks.filter((task) => task.id !== id));
};
return (
<div className="w-full max-w-2xl mx-auto p-6 bg-white rounded-lg shadow-xl border border-gray-200">
<h3 className="text-3xl font-bold text-gray-800 mb-6">Your Tasks</h3>
{/* Task Input Form */}
<form onSubmit={addTask} className="flex flex-col md:flex-row gap-4 mb-8">
<input
type="text"
placeholder="Add a new task..."
value={newTaskTitle}
onChange={(e) => setNewTaskTitle(e.target.value)}
className="flex-grow p-3 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 outline-none"
/>
<button
type="submit"
className="px-6 py-3 bg-blue-600 text-white rounded-md shadow-md hover:bg-blue-700 transition duration-300 transform hover:scale-105"
>
Add Task
</button>
</form>
{/* Task List */}
{tasks.length === 0 ? (
<p className="text-center text-gray-500 italic">No tasks yet. Start adding some!</p>
) : (
<ul className="space-y-4">
{tasks.map((task) => (
<li
key={task.id}
className="flex items-center justify-between p-4 bg-gray-50 border border-gray-200 rounded-md shadow-sm transition duration-200 hover:bg-gray-100"
>
<div className="flex items-start flex-grow"> {/* Use flex-start to align checkbox and text */}
<input
type="checkbox"
checked={task.completed}
onChange={() => toggleTaskCompletion(task.id)}
className="form-checkbox h-5 w-5 text-blue-600 rounded mr-3 mt-1 cursor-pointer flex-shrink-0"
/>
<div className="flex flex-col flex-grow">
<span
className={`text-lg font-medium ${
task.completed ? 'line-through text-gray-500' : 'text-gray-900'
}`}
>
{task.title}
</span>
<p className="text-sm text-gray-500 mt-1">
Priority: <span className="font-semibold">{task.priority}</span> | Due: <span className="font-semibold">{task.dueDate}</span>
</p>
<p className="text-sm text-gray-600 mt-1 italic">{task.description}</p>
</div>
</div>
<button
onClick={() => deleteTask(task.id)}
className="ml-4 px-3 py-1 bg-red-500 text-white rounded-md text-sm shadow-sm hover:bg-red-600 transition duration-300 flex-shrink-0"
>
Delete
</button>
</li>
))}
</ul>
)}
</div>
);
}
4. Create app/dashboard/loading.tsx¶
To provide a good user experience during the data fetching delay, we'll create a loading skeleton for our dashboard.
-
Create a new file
loading.tsxinside yourapp/dashboard/folder:my-task-app/ └── app/ ├── ... └── dashboard/ ├── layout.tsx ├── page.tsx ├── loading.tsx <-- NEW FILE -
Add the following content to
app/dashboard/loading.tsx:app/dashboard/loading.tsx// app/dashboard/loading.tsx // This file defines the loading UI for the /dashboard route segment. // It will be displayed automatically while data is being fetched for page.tsx. import React from 'react'; export default function DashboardLoading() { return ( <div className="flex flex-col items-center justify-center p-8 bg-white rounded-xl shadow-lg border border-gray-200 animate-pulse"> <h2 className="text-3xl font-bold text-gray-400 mb-6">Loading Your Dashboard...</h2> <div className="h-6 bg-gray-200 rounded w-3/4 mb-4"></div> <div className="h-4 bg-gray-200 rounded w-1/2 mb-8"></div> <div className="w-full max-w-2xl mx-auto space-y-4"> {/* Skeleton for task input form */} <div className="flex flex-col md:flex-row gap-4 mb-8"> <div className="h-12 bg-gray-200 rounded-md flex-grow"></div> <div className="h-12 w-32 bg-gray-300 rounded-md"></div> </div> {/* Skeletons for individual tasks */} {[...Array(3)].map((_, i) => ( // Render 3 skeleton items <div key={i} className="flex items-center justify-between p-4 bg-gray-100 border border-gray-200 rounded-md shadow-sm"> <div className="flex items-start flex-grow"> <div className="h-5 w-5 bg-gray-300 rounded-full mr-3 mt-1"></div> <div className="flex flex-col flex-grow"> <div className="h-6 bg-gray-300 rounded w-3/4 mb-2"></div> <div className="h-4 bg-gray-200 rounded w-1/2"></div> </div> </div> <div className="ml-4 h-8 w-16 bg-gray-300 rounded-md"></div> </div> ))} </div> </div> ); }
What we've done¶
- Mock API: Created a basic
GETRoute Handler at/api/tasksthat returns hardcoded task data with a simulated delay. - Server-Side Fetching: Modified
app/dashboard/page.tsxto be anasyncServer Component that fetches data from our mock API. We usedcache: 'no-store'to enforce SSR behavior. - Prop-Based TaskList: Updated
components/TaskList.tsxto receiveinitialTasksas a prop, making it a pure UI component (for initial render) while still allowing client-side interactions for now. - Loading UI: Implemented
app/dashboard/loading.tsxto display a skeleton loader while the tasks are being fetched, significantly improving user experience.
To test the new data fetching and loading states:
- Ensure your development server is running (
npm run dev). -
If you haven't already, create a
.env.localfile in your project root and add:NEXT_PUBLIC_BASE_URL=http://localhost:3000Then restart your dev server (
npm run dev) if it was already running. -
Navigate to
http://localhost:3000/dashboard.
You should now briefly see a loading skeleton on the dashboard page before the mock tasks appear. This demonstrates a fundamental pattern for data fetching in Next.js using Server Components and handling loading states gracefully. In the next module, we'll learn about data mutations using Server Actions, which provide a streamlined way to update data on the server without explicit API routes.