RSC Mini Project — Building a Complete Server Components Application
In this tutorial, you will learn about RSC Mini Project. We cover key concepts, practical examples, and best practices to help you master this topic.
This mini project guides you through building a complete task management application using React Server Components, combining all concepts from this course.
What You'll Learn
You will apply RSC concepts in a real project: Server Component data fetching, Client Components for interactivity, Server Actions for mutations, streaming with Suspense, and Caching.
Why It Matters
Building a complete project solidifies your understanding of how RSC concepts work together. You will have a working application you can extend and deploy.
Real-World Use
DodaZIP uses a similar task management system internally for tracking bug reports and feature requests, with Server Components rendering the task list and Server Actions handling updates.
flowchart TD
A[Task Manager App] --> B[Server Components]
A --> C[Client Components]
A --> D[Server Actions]
B --> E[Task List Page]
B --> F[Task Detail]
B --> G[Stats Summary]
C --> H[Task Form]
C --> I[Filter Bar]
C --> J[Status Toggle]
D --> K[createTask]
D --> L[updateTask]
D --> M[deleteTask]
style B fill:#1e293b,color:#fff
style C fill:#0f172a,color:#fff
style D fill:#0f172a,color:#fff
Project Setup
Create a new Next.js project with the App Router.
npx create-next-app@latest task-manager --app
cd task-manager
npm install mongoose
Set up a simple data model for tasks.
// lib/models/task.js
import mongoose from 'mongoose';
const TaskSchema = new mongoose.Schema({
title: { type: String, required: true },
description: { type: String, default: '' },
status: { type: String, enum: ['todo', 'in-progress', 'done'], default: 'todo' },
priority: { type: String, enum: ['low', 'medium', 'high'], default: 'medium' },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now },
});
export const Task = mongoose.models.Task || mongoose.model('Task', TaskSchema);
Expected output: A Next.js project with a Mongoose task model. The model has title, description, status, priority, and timestamps.
Server Component Task List
Create a Server Component that fetches and renders the task list.
// app/page.js
import { Suspense } from 'react';
import { Task } from '@/lib/models';
import { connectDB } from '@/lib/db';
import TaskList from './TaskList';
import TaskForm from './TaskForm';
import StatsBar from './StatsBar';
async function getTasks() {
await connectDB();
const tasks = await Task.find().sort({ createdAt: -1 }).lean();
return tasks.map(t => ({
...t,
_id: t._id.toString(),
createdAt: t.createdAt.toISOString(),
updatedAt: t.updatedAt.toISOString(),
}));
}
export default async function HomePage() {
const tasks = await getTasks();
const stats = {
total: tasks.length,
todo: tasks.filter(t => t.status === 'todo').length,
inProgress: tasks.filter(t => t.status === 'in-progress').length,
done: tasks.filter(t => t.status === 'done').length,
};
return (
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '24px' }}>
<h1>Task Manager</h1>
<StatsBar stats={stats} />
<TaskForm />
<Suspense fallback={<p>Loading tasks...</p>}>
<TaskList initialTasks={tasks} />
</Suspense>
</div>
);
}
Expected output: The home page fetches all tasks from the database, computes stats, and renders the stats bar, task form, and task list. The task list is wrapped in Suspense for streaming.
Client Components
Create the interactive Client Components for the task manager.
'use client';
// app/TaskForm.jsx
import { useActionState } from 'react';
import { createTask } from './actions';
export default function TaskForm() {
const [state, formAction, pending] = useActionState(createTask, null);
return (
<form action={formAction} style={{ margin: '16px 0', padding: '16px', border: '1px solid #ddd' }}>
<div style={{ marginBottom: '8px' }}>
<input name="title" placeholder="Task title" required style={{ width: '100%', padding: '8px' }} />
</div>
<div style={{ marginBottom: '8px' }}>
<textarea name="description" placeholder="Description" style={{ width: '100%', padding: '8px' }} />
</div>
<div style={{ marginBottom: '8px' }}>
<select name="priority" style={{ padding: '8px' }}>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<button type="submit" disabled={pending} style={{ padding: '8px 16px' }}>
{pending ? 'Adding...' : 'Add Task'}
</button>
{state?.error && <p style={{ color: 'red' }}>{state.error}</p>}
{state?.success && <p style={{ color: 'green' }}>Task added!</p>}
</form>
);
}
'use client';
// app/TaskList.jsx
import { useOptimistic, useState } from 'react';
import { updateTask, deleteTask } from './actions';
export default function TaskList({ initialTasks }) {
const [filter, setFilter] = useState('all');
const [tasks, setTasks] = useState(initialTasks);
const filteredTasks = tasks.filter(t => filter === 'all' ? true : t.status === filter);
const statusColors = { todo: '#ffd700', 'in-progress': '#87ceeb', done: '#90ee90' };
async function handleStatusChange(taskId, newStatus) {
setTasks(prev => prev.map(t => t._id === taskId ? { ...t, status: newStatus } : t));
await updateTask(taskId, { status: newStatus });
}
async function handleDelete(taskId) {
setTasks(prev => prev.filter(t => t._id !== taskId));
await deleteTask(taskId);
}
return (
<div>
<div style={{ margin: '16px 0' }}>
<button onClick={() => setFilter('all')} style={{ marginRight: '8px' }}>All</button>
<button onClick={() => setFilter('todo')} style={{ marginRight: '8px' }}>Todo</button>
<button onClick={() => setFilter('in-progress')} style={{ marginRight: '8px' }}>In Progress</button>
<button onClick={() => setFilter('done')}>Done</button>
</div>
<ul style={{ listStyle: 'none', padding: 0 }}>
{filteredTasks.map(task => (
<li key={task._id} style={{ padding: '12px', margin: '8px 0', border: '1px solid #ddd', borderLeft: `4px solid ${statusColors[task.status]}` }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<strong>{task.title}</strong>
<span style={{ fontSize: '12px', color: '#666' }}>{task.priority}</span>
</div>
{task.description && <p style={{ margin: '4px 0', color: '#555' }}>{task.description}</p>}
<div style={{ marginTop: '8px' }}>
<select value={task.status} onChange={e => handleStatusChange(task._id, e.target.value)}>
<option value="todo">Todo</option>
<option value="in-progress">In Progress</option>
<option value="done">Done</option>
</select>
<button onClick={() => handleDelete(task._id)} style={{ marginLeft: '8px', color: 'red' }}>Delete</button>
</div>
</li>
))}
</ul>
</div>
);
}
Expected output: Interactive task list with filter buttons, status dropdown, and delete button. Optimistic updates change the UI immediately before the Server Action completes.
Server Actions
Create the Server Actions for task mutations.
// app/actions.js
'use server';
import { Task } from '@/lib/models';
import { connectDB } from '@/lib/db';
import { revalidatePath } from 'next/cache';
export async function createTask(prevState, formData) {
await connectDB();
const title = formData.get('title');
const description = formData.get('description');
const priority = formData.get('priority') || 'medium';
if (!title || title.length < 2) {
return { error: 'Title must be at least 2 characters' };
}
await Task.create({ title, description, priority });
revalidatePath('/');
return { success: true };
}
export async function updateTask(taskId, data) {
await connectDB();
await Task.findByIdAndUpdate(taskId, { ...data, updatedAt: new Date() });
revalidatePath('/');
return { success: true };
}
export async function deleteTask(taskId) {
await connectDB();
await Task.findByIdAndDelete(taskId);
revalidatePath('/');
return { success: true };
}
Expected output: Three Server Actions handle creating, updating, and deleting tasks. Each action connects to the database, performs the mutation, and revalidates the page cache.
Testing the Application
Run the application and test all features.
npm run dev
Expected output: A fully functional task manager with:
- Server Component fetching all tasks from the database
- Stats bar showing task counts by status
- Form to add new tasks with validation
- Filter buttons to show tasks by status
- Status dropdown to change task status
- Delete button to remove tasks
- Optimistic updates for instant UI feedback
- All mutations handled by Server Actions
- Page revalidation after each mutation
Common Mistakes
Not handling the database connection properly: Always await connectDB before each query. Use a cached connection pattern to avoid multiple connections.
Forgetting to serialize MongoDB ObjectIds: Convert _id to string before passing to Client Components. ObjectIds are not serializable.
Not revalidating after mutations: Without revalidatePath, the page shows stale data. Always revalidate after creating, updating, or deleting.
Making the entire page a Client Component: The task list data fetching should stay in a Server Component. Only the form and interactive list need to be Client Components.
Not handling errors in Server Actions: Always wrap database operations in try/catch and return structured error responses.
Practice Questions
- Why do we convert _id to string in the Server Component?
MongoDB ObjectIds are not serializable. They must be converted to strings before passing to Client Components.
- What is the purpose of revalidatePath('/') in each Server Action?
It tells Next.js to re-fetch and re-render the home page after a mutation, ensuring the task list shows updated data.
- How does the optimistic update pattern work in TaskList?
When the user changes a status, we immediately update the local state. The Server Action runs in the background. If it fails, we would revert the change.
- Why is TaskForm a Client Component?
It uses useActionState for form state management and needs to handle user input and button clicks interactively.
- How could you add authentication to this task manager?
Read session cookies in the Server Component, pass user data to a Client Component provider, and check authentication in each Server Action.
Challenge
Extend the task manager with: user authentication (each user sees only their tasks), due dates, task categories, drag-and-drop reordering, and a real-time collaboration feature using Server-Sent Events.
What's Next
Congratulations on completing the React Server Components course. Apply these skills to build performant, interactive applications. Continue with Next.js Guide for more routing and deployment patterns.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro