Loading lessons...
Project: Server Actions Dashboard & Analytics
Project: Server Actions Dashboard & Analytics
In this project, you will build an interactive Task & Analytics Dashboard powered by Server Actions, useActionState, Optimistic UI, and Skeleton Loading boundaries.
Project Requirements
- Instant optimistic updates when creating tasks.
- Background mutation using Server Actions (
'use server'). - Skeleton loader in
loading.tsxusing React Suspense.
1. Backend Server Actions (app/actions/tasks.ts)
'use server';
import { revalidatePath } from 'next/cache';
export type Task = { id: string; title: string; completed: boolean };
let mockTasks: Task[] = [
{ id: '1', title: 'Review Pull Request', completed: false },
{ id: '2', title: 'Deploy Next.js 15 App', completed: true },
];
export async function getTasks(): Promise<Task[]> {
// Simulate network latency
await new Promise((r) => setTimeout(r, 800));
return mockTasks;
}
export async function addTaskAction(prevState: any, formData: FormData) {
const title = formData.get('title') as string;
if (!title || title.length < 3) {
return { error: 'Task title must be at least 3 characters long' };
}
const newTask: Task = { id: Date.now().toString(), title, completed: false };
mockTasks.push(newTask);
revalidatePath('/dashboard');
return { success: true };
}
2. Client Task Form with Optimistic UI (components/TaskForm.tsx)
'use client';
import { useActionState, useOptimistic, useRef } from 'react';
import { addTaskAction, Task } from '@/app/actions/tasks';
export default function TaskForm({ initialTasks }: { initialTasks: Task[] }) {
const formRef = useRef<HTMLFormElement>(null);
const [state, formAction] = useActionState(addTaskAction, null);
const [optimisticTasks, addOptimisticTask] = useOptimistic(
initialTasks,
(state: Task[], title: string) => [
...state,
{ id: 'temp-' + Date.now(), title, completed: false },
]
);
async function handleSubmit(formData: FormData) {
const title = formData.get('title') as string;
if (title) {
addOptimisticTask(title);
formRef.current?.reset();
}
await formAction(formData);
}
return (
<div className="space-y-4">
<form ref={formRef} action={handleSubmit} className="flex gap-2">
<input name="title" placeholder="New Task..." className="border p-2 flex-1 rounded" required />
<button type="submit" className="bg-indigo-600 text-white px-4 py-2 rounded">
Add Task
</button>
</form>
{state?.error && <p className="text-red-500 text-sm">{state.error}</p>}
<ul className="space-y-2">
{optimisticTasks.map((task) => (
<li key={task.id} className="p-3 border rounded bg-slate-50 flex items-center justify-between">
<span>{task.title}</span>
{task.id.startsWith('temp-') && (
<span className="text-xs text-amber-600 animate-pulse">Syncing...</span>
)}
</li>
))}
</ul>
</div>
);
}
3. Dashboard Skeleton Loader (app/dashboard/loading.tsx)
export default function LoadingDashboard() {
return (
<div className="p-8 space-y-4 animate-pulse">
<div className="h-8 w-64 bg-slate-200 rounded"></div>
<div className="h-12 w-full bg-slate-200 rounded"></div>
<div className="h-32 w-full bg-slate-200 rounded"></div>
</div>
);
}
TL;DR
- Combine
useOptimisticwithuseActionStatefor high-performance form UIs. - Skeleton loaders in
loading.tsxprovide instant visual feedback while RSC data fetches. - Revalidate paths dynamically in Server Actions using
revalidatePath().