Back to articles
Next.js

Next.js 15: Complete Step-by-Step Guide from Setup to Production

Build a complete task management app with Next.js 15, React 19 hooks, Turbopack, authentication, database, and deploy to production. Follow along from zero to live application.

25 mins read
Next.jsReact 19TurbopackFull-StackProduction

Next.js 15 represents a revolutionary leap forward in web development. In this comprehensive guide, we'll build a complete task management application from scratch, taking you through every step from initial setup to production deployment. You'll learn React 19 hooks, Turbopack optimization, authentication, database integration, and modern deployment strategies.

💡

🚀 Interactive Learning Experience

React 19 useActionState Hook - Interactive form with validation and loading states
Next.js Server Actions Demo - Full-stack book manager with progressive enhancement
useOptimistic Hook Demo - Todo app with instant UI updates and simulated delays
All examples are fully functional and editable in your browser
Experiment with the code to deepen your understanding
Each demo includes helpful tips and real-world scenarios

What We'll Build: TaskFlow - A Complete Task Management App

Our production-ready application will include user authentication, real-time task management, optimistic UI updates, advanced caching, and deployment to Vercel. By the end of this guide, you'll have a fully functional app and deep understanding of Next.js 15.

User Authentication

Secure login/signup with better-auth and session management

Real-time Updates

Live task updates using Server-Sent Events and WebSockets

Optimistic UI

Instant feedback with React 19 useOptimistic hook

Production Deployment

Complete CI/CD pipeline with Vercel and database hosting

Phase 1: Project Setup and Installation

Let's start by creating our Next.js 15 project with all necessary dependencies. We'll configure Turbopack for lightning-fast development and set up our development environment.

Step 1: Create Next.js 15 Project

Terminal - Project Creation
# Create new Next.js 15 project with all modern featuresnpx create-next-app@latest taskflow-app \  --typescript \  --tailwind \  --eslint \  --app \  --src-dir \  --import-alias "@/*"# Navigate to project directorycd taskflow-app# Verify Next.js 15 installationnpm list next# Should show: next@15.x.x

Step 2: Install Dependencies

Terminal - Dependencies Installation
# Core dependencies for our full-stack appnpm install \  better-auth \  @better-fetch/fetch \  drizzle-orm \  postgres \  @neondatabase/serverless \  @paralleldrive/cuid2 \  bcryptjs \  zod \  react-hook-form \  @hookform/resolvers \  framer-motion \  lucide-react \  @radix-ui/react-dialog \  @radix-ui/react-dropdown-menu \  @radix-ui/react-toast \  @radix-ui/react-select \  date-fns \  @tanstack/react-query# Development dependenciesnpm install -D \  drizzle-kit \  @types/bcryptjs \  @types/node \  tsx \  @next/bundle-analyzer

Step 3: Configure Next.js 15 with Turbopack

Configure your project to leverage Next.js 15's performance improvements and new features:

Next.jsnext.config.ts
import type { NextConfig } from 'next';const nextConfig: NextConfig = {  // Enable Turbopack for development (10x faster than Webpack)  experimental: {    turbo: {      resolveAlias: {        '@': './src',        '@/components': './src/components',        '@/lib': './src/lib',        '@/app': './src/app',        '@/types': './src/types',      },      // CSS optimization      optimizeCss: true,      // Tree shaking for smaller bundles      treeShaking: true,      // Module imports optimization      modularizeImports: {        'lucide-react': {          transform: 'lucide-react/dist/esm/icons/{{member}}',        },        '@radix-ui/react-icons': {          transform: '@radix-ui/react-icons/dist/{{member}}.js',        },      },    },  },    // TypeScript configuration  typescript: {    tsconfigPath: './tsconfig.json',    ignoreBuildErrors: false,  },    // Image optimization for better performance  images: {    formats: ['image/avif', 'image/webp'],    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],  },    // Environment variables  env: {    BETTER_AUTH_URL: process.env.BETTER_AUTH_URL,    BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET,  },};export default nextConfig;
package.json - Updated Scripts
{  "scripts": {    "dev": "next dev --turbo",    "dev:webpack": "next dev",    "build": "next build",    "start": "next start",    "lint": "next lint",    "type-check": "tsc --noEmit",    "analyze": "ANALYZE=true npm run build",    "db:generate": "drizzle-kit generate",    "db:push": "drizzle-kit push",    "db:studio": "drizzle-kit studio",    "db:migrate": "drizzle-kit migrate"  }}

Phase 2: Database Setup with Drizzle ORM

Set up a PostgreSQL database with Drizzle ORM for our task management system. We'll create schemas for users, tasks, and sessions using Drizzle's type-safe approach.

Step 4: Initialize Database

Terminal - Database Setup
# Create database directoriesmkdir -p src/dbmkdir -p drizzle# Initialize Drizzle configurationtouch drizzle.config.tstouch src/db/schema.tstouch src/db/index.ts# Create environment filetouch .env.local
TypeScriptdrizzle.config.ts
import { defineConfig } from "drizzle-kit";export default defineConfig({  schema: "./src/db/schema.ts",  out: "./drizzle",  dialect: "postgresql",  dbCredentials: {    url: process.env.DATABASE_URL!,  },  verbose: true,  strict: true,});
TypeScriptsrc/db/schema.ts
import { pgTable, text, timestamp, boolean, pgEnum, index, uniqueIndex } from "drizzle-orm/pg-core";import { createId } from "@paralleldrive/cuid2";// Enumsexport const priorityEnum = pgEnum("priority", ["LOW", "MEDIUM", "HIGH", "URGENT"]);export const statusEnum = pgEnum("status", ["TODO", "IN_PROGRESS", "REVIEW", "COMPLETED"]);// Users tableexport const users = pgTable("users", {  id: text("id").primaryKey().$defaultFn(() => createId()),  email: text("email").notNull().unique(),  name: text("name"),  password: text("password"),  avatar: text("avatar"),  emailVerified: timestamp("email_verified"),  createdAt: timestamp("created_at").defaultNow().notNull(),  updatedAt: timestamp("updated_at").defaultNow().notNull(),}, (table) => ({  emailIdx: uniqueIndex("users_email_idx").on(table.email),}));// Sessions table for better-authexport const sessions = pgTable("sessions", {  id: text("id").primaryKey().$defaultFn(() => createId()),  sessionToken: text("session_token").notNull().unique(),  userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),  expires: timestamp("expires").notNull(),  createdAt: timestamp("created_at").defaultNow().notNull(),}, (table) => ({  sessionTokenIdx: uniqueIndex("sessions_token_idx").on(table.sessionToken),  userIdIdx: index("sessions_user_id_idx").on(table.userId),}));// Accounts table for OAuth providersexport const accounts = pgTable("accounts", {  id: text("id").primaryKey().$defaultFn(() => createId()),  userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),  type: text("type").notNull(),  provider: text("provider").notNull(),  providerAccountId: text("provider_account_id").notNull(),  refreshToken: text("refresh_token"),  accessToken: text("access_token"),  expiresAt: timestamp("expires_at"),  tokenType: text("token_type"),  scope: text("scope"),  idToken: text("id_token"),  sessionState: text("session_state"),  createdAt: timestamp("created_at").defaultNow().notNull(),}, (table) => ({  providerAccountIdx: uniqueIndex("accounts_provider_account_idx").on(table.provider, table.providerAccountId),  userIdIdx: index("accounts_user_id_idx").on(table.userId),}));// Categories tableexport const categories = pgTable("categories", {  id: text("id").primaryKey().$defaultFn(() => createId()),  name: text("name").notNull(),  color: text("color").notNull().default("#3B82F6"),  createdAt: timestamp("created_at").defaultNow().notNull(),});// Tasks tableexport const tasks = pgTable("tasks", {  id: text("id").primaryKey().$defaultFn(() => createId()),  title: text("title").notNull(),  description: text("description"),  completed: boolean("completed").notNull().default(false),  priority: priorityEnum("priority").notNull().default("MEDIUM"),  status: statusEnum("status").notNull().default("TODO"),  dueDate: timestamp("due_date"),  createdAt: timestamp("created_at").defaultNow().notNull(),  updatedAt: timestamp("updated_at").defaultNow().notNull(),  userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),  categoryId: text("category_id").references(() => categories.id),}, (table) => ({  userIdIdx: index("tasks_user_id_idx").on(table.userId),  completedIdx: index("tasks_completed_idx").on(table.completed),  priorityIdx: index("tasks_priority_idx").on(table.priority),  categoryIdIdx: index("tasks_category_id_idx").on(table.categoryId),}));// Type exports for TypeScriptexport type User = typeof users.$inferSelect;export type NewUser = typeof users.$inferInsert;export type Session = typeof sessions.$inferSelect;export type NewSession = typeof sessions.$inferInsert;export type Account = typeof accounts.$inferSelect;export type NewAccount = typeof accounts.$inferInsert;export type Task = typeof tasks.$inferSelect;export type NewTask = typeof tasks.$inferInsert;export type Category = typeof categories.$inferSelect;export type NewCategory = typeof categories.$inferInsert;
TypeScriptsrc/db/index.ts
import { drizzle } from "drizzle-orm/neon-serverless";import { Pool } from "@neondatabase/serverless";import * as schema from "./schema";// Create connection poolconst pool = new Pool({ connectionString: process.env.DATABASE_URL });// Create Drizzle instanceexport const db = drizzle(pool, { schema });// Export schema for easier importsexport { schema };export * from "./schema";

Step 5: Environment Configuration

.env.local
# Database (for development, use local PostgreSQL or Docker)DATABASE_URL="postgresql://username:password@localhost:5432/taskflow"# For production, use Neon, Vercel Postgres or your preferred provider# DATABASE_URL="postgres://username:password@host:5432/database"# better-auth configurationBETTER_AUTH_SECRET="your-super-secret-key-here-change-this"BETTER_AUTH_URL="http://localhost:3000"# Optional: OAuth providers (Google, GitHub, etc.)GOOGLE_CLIENT_ID="your-google-client-id"GOOGLE_CLIENT_SECRET="your-google-client-secret"GITHUB_CLIENT_ID="your-github-client-id"GITHUB_CLIENT_SECRET="your-github-client-secret"
Terminal - Database Migration
# Generate migration filesnpx drizzle-kit generate# Push schema to database (creates tables)npx drizzle-kit push# Optional: Open Drizzle Studio to view datanpx drizzle-kit studio

Phase 3: Authentication with better-auth

Implement secure authentication with email/password and OAuth providers using better-auth, a modern TypeScript-first authentication library.

Step 6: Configure better-auth

TypeScriptsrc/lib/auth.ts
import { betterAuth } from "better-auth";import { drizzleAdapter } from "better-auth/adapters/drizzle";import { db } from "@/db";import { users, sessions, accounts } from "@/db/schema";export const auth = betterAuth({  database: drizzleAdapter(db, {    provider: "pg",    schema: {      users,      sessions,      accounts,    },  }),    emailAndPassword: {    enabled: true,    requireEmailVerification: false, // Set to true in production  },    socialProviders: {    google: {      clientId: process.env.GOOGLE_CLIENT_ID!,      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,    },    github: {      clientId: process.env.GITHUB_CLIENT_ID!,      clientSecret: process.env.GITHUB_CLIENT_SECRET!,    },  },    session: {    expiresIn: 60 * 60 * 24 * 7, // 7 days    updateAge: 60 * 60 * 24, // 24 hours  },    baseURL: process.env.BETTER_AUTH_URL,  secret: process.env.BETTER_AUTH_SECRET,    logger: {    level: process.env.NODE_ENV === "development" ? "debug" : "error",  },});export type Session = typeof auth.$Infer.Session;export type User = typeof auth.$Infer.User;
TypeScriptsrc/lib/auth-client.ts
import { createAuthClient } from "better-auth/react";export const authClient = createAuthClient({  baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL || "http://localhost:3000",});export const { signIn, signOut, signUp, useSession } = authClient;

Step 7: Create API Routes

TypeScriptsrc/app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";export const { GET, POST } = auth.handler;

Note: With better-auth, user registration is handled automatically through the authentication system. You can customize the registration flow by using better-auth's built-in signup functionality or create custom registration logic if needed.

Phase 4: Building the UI with React 19 Hooks

Create a modern, responsive UI using React 19 hooks, Tailwind CSS, and Radix UI components. We'll implement the new useActionState, useFormStatus, and useOptimistic hooks.

React 19 useActionState Hook Demo

Try this interactive example of the new useActionState hook in action!

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

const container = document.getElementById('root');
if (container) {
  const root = createRoot(container);
  root.render(<App />);
}

Step 8: Create Layout and Providers

Reactsrc/app/layout.tsx
import './globals.css'import type { Metadata } from 'next'import { Inter } from 'next/font/google'import { QueryProvider } from '@/components/providers/query-provider'import { ThemeProvider } from '@/components/providers/theme-provider'import { Toaster } from '@/components/ui/toaster'const inter = Inter({ subsets: ['latin'] })export const metadata: Metadata = {  title: 'TaskFlow - Modern Task Management',  description: 'A modern task management application built with Next.js 15',}export default function RootLayout({  children,}: {  children: React.ReactNode}) {  return (    <html lang="en" suppressHydrationWarning>      <body className={inter.className}>        <QueryProvider>          <ThemeProvider            attribute="class"            defaultTheme="system"            enableSystem            disableTransitionOnChange          >            {children}            <Toaster />          </ThemeProvider>        </QueryProvider>      </body>    </html>  )}

Step 9: Implement React 19 useActionState Hook

Create a task creation form using the new useActionState hook for seamless form handling:

Reactsrc/components/task-form.tsx
'use client'import { useActionState } from 'react'import { useFormStatus } from 'react-dom'import { createTask } from '@/app/actions/tasks'import { Button } from '@/components/ui/button'import { Input } from '@/components/ui/input'import { Textarea } from '@/components/ui/textarea'import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'import { Loader2, Plus } from 'lucide-react'function SubmitButton() {  const { pending } = useFormStatus()    return (    <Button type="submit" disabled={pending} className="w-full">      {pending ? (        <>          <Loader2 className="w-4 h-4 mr-2 animate-spin" />          Creating task...        </>      ) : (        <>          <Plus className="w-4 h-4 mr-2" />          Create Task        </>      )}    </Button>  )}export function TaskForm() {  const [state, formAction] = useActionState(createTask, {    message: '',    errors: {},  })  return (    <form action={formAction} className="space-y-4">      <div>        <Input          name="title"          placeholder="Task title"          required          className="w-full"        />        {state.errors?.title && (          <p className="text-sm text-red-600 mt-1">{state.errors.title}</p>        )}      </div>      <div>        <Textarea          name="description"          placeholder="Task description (optional)"          className="w-full"        />        {state.errors?.description && (          <p className="text-sm text-red-600 mt-1">{state.errors.description}</p>        )}      </div>      <div className="grid grid-cols-2 gap-4">        <div>          <Select name="priority" defaultValue="MEDIUM">            <SelectTrigger>              <SelectValue placeholder="Priority" />            </SelectTrigger>            <SelectContent>              <SelectItem value="LOW">Low</SelectItem>              <SelectItem value="MEDIUM">Medium</SelectItem>              <SelectItem value="HIGH">High</SelectItem>              <SelectItem value="URGENT">Urgent</SelectItem>            </SelectContent>          </Select>        </div>        <div>          <Input            name="dueDate"            type="date"            className="w-full"          />        </div>      </div>      <SubmitButton />      {state.message && (        <div className="p-3 bg-green-50 border border-green-200 rounded-md">          <p className="text-green-800 text-sm">{state.message}</p>        </div>      )}      {state.errors?._form && (        <div className="p-3 bg-red-50 border border-red-200 rounded-md">          <p className="text-red-800 text-sm">{state.errors._form}</p>        </div>      )}    </form>  )}

Step 10: Server Actions with Enhanced Error Handling

TypeScriptsrc/app/actions/tasks.ts
'use server'import { z } from 'zod'import { revalidatePath } from 'next/cache'import { auth } from '@/lib/auth'import { db } from '@/db'import { tasks } from '@/db/schema'import { eq, and } from 'drizzle-orm'import { headers } from 'next/headers'const createTaskSchema = z.object({  title: z.string().min(1, 'Title is required').max(100, 'Title too long'),  description: z.string().max(500, 'Description too long').optional(),  priority: z.enum(['LOW', 'MEDIUM', 'HIGH', 'URGENT']).default('MEDIUM'),  dueDate: z.string().optional(),})export async function createTask(prevState: any, formData: FormData) {  const session = await auth.api.getSession({    headers: await headers(),  })    if (!session?.user?.id) {    return {      errors: { _form: 'You must be logged in to create tasks' },      message: '',    }  }  const validatedFields = createTaskSchema.safeParse({    title: formData.get('title'),    description: formData.get('description'),    priority: formData.get('priority'),    dueDate: formData.get('dueDate'),  })  if (!validatedFields.success) {    return {      errors: validatedFields.error.flatten().fieldErrors,      message: '',    }  }  const { title, description, priority, dueDate } = validatedFields.data  try {    await db.insert(tasks).values({      title,      description: description || null,      priority,      dueDate: dueDate ? new Date(dueDate) : null,      userId: session.user.id,    })    revalidatePath('/dashboard')        return {      message: 'Task created successfully!',      errors: {},    }  } catch (error) {    console.error('Failed to create task:', error)    return {      errors: { _form: 'Failed to create task. Please try again.' },      message: '',    }  }}export async function toggleTask(taskId: string) {  const session = await auth.api.getSession({    headers: await headers(),  })    if (!session?.user?.id) {    throw new Error('Unauthorized')  }  try {    const task = await db.select({       userId: tasks.userId,       completed: tasks.completed     })    .from(tasks)    .where(eq(tasks.id, taskId))    .limit(1)    if (!task[0] || task[0].userId !== session.user.id) {      throw new Error('Task not found or unauthorized')    }    await db.update(tasks)      .set({         completed: !task[0].completed,        updatedAt: new Date()      })      .where(eq(tasks.id, taskId))    revalidatePath('/dashboard')  } catch (error) {    console.error('Failed to toggle task:', error)    throw error  }}export async function deleteTask(taskId: string) {  const session = await auth.api.getSession({    headers: await headers(),  })    if (!session?.user?.id) {    throw new Error('Unauthorized')  }  try {    const task = await db.select({ userId: tasks.userId })      .from(tasks)      .where(eq(tasks.id, taskId))      .limit(1)    if (!task[0] || task[0].userId !== session.user.id) {      throw new Error('Task not found or unauthorized')    }    await db.delete(tasks).where(eq(tasks.id, taskId))    revalidatePath('/dashboard')  } catch (error) {    console.error('Failed to delete task:', error)    throw error  }}

useActionState Hook Demo

Experience form handling with React 19's useActionState hook! This demo shows async form validation and state management.

import { useState } from 'react';
import { BookForm } from './BookForm';
import { BookList } from './BookList';

interface Book {
  id: number;
  title: string;
  author: string;
  rating: number;
}

export default function App(): JSX.Element {
  const [books, setBooks] = useState<Book[]>([
    { id: 1, title: 'Next.js Guide', author: 'John Doe', rating: 5 },
    { id: 2, title: 'React Patterns', author: 'Jane Smith', rating: 4 }
  ]);

  const addBook = (book: Book) => {
    setBooks(prev => [...prev, book]);
  };

  return (
    <div style={{ 
      maxWidth: '600px', 
      margin: '20px auto', 
      padding: '20px',
      fontFamily: 'system-ui, sans-serif'
    }}>
      <h1 style={{ 
        textAlign: 'center', 
        marginBottom: '30px',
        color: '#1f2937'
      }}>
        📚 Book Collection Manager
      </h1>
      
      <div style={{
        backgroundColor: '#f8fafc',
        padding: '20px',
        borderRadius: '12px',
        border: '1px solid #e2e8f0',
        marginBottom: '30px'
      }}>
        <h2 style={{ marginBottom: '16px', color: '#374151' }}>
          Add New Book
        </h2>
        <BookForm onAddBook={addBook} />
      </div>
      
      <div>
        <h2 style={{ marginBottom: '16px', color: '#374151' }}>
          Your Books ({books.length})
        </h2>
        <BookList books={books} />
      </div>
      
      <div style={{
        marginTop: '24px',
        padding: '16px',
        backgroundColor: '#dbeafe',
        borderRadius: '8px',
        fontSize: '14px',
        color: '#1e40af'
      }}>
        💡 <strong>useActionState Demo:</strong> This showcases form handling patterns 
        similar to Next.js Server Actions with client-side state management!
      </div>
    </div>
  );
}

Step 11: Optimistic UI with useOptimistic Hook

Implement instant UI updates using React 19's useOptimistic hook:

Reactsrc/components/task-list.tsx
'use client'import { useOptimistic, useTransition } from 'react'import { Task } from '@/db/schema'import { toggleTask, deleteTask } from '@/app/actions/tasks'import { Button } from '@/components/ui/button'import { Checkbox } from '@/components/ui/checkbox'import { Trash2, Calendar, Flag } from 'lucide-react'import { format } from 'date-fns'interface TaskListProps {  initialTasks: Task[]}type OptimisticAction =   | { type: 'toggle'; id: string }  | { type: 'delete'; id: string }export function TaskList({ initialTasks }: TaskListProps) {  const [isPending, startTransition] = useTransition()    const [optimisticTasks, addOptimisticTask] = useOptimistic(    initialTasks,    (state, action: OptimisticAction) => {      switch (action.type) {        case 'toggle':          return state.map(task =>            task.id === action.id               ? { ...task, completed: !task.completed }              : task          )        case 'delete':          return state.filter(task => task.id !== action.id)        default:          return state      }    }  )  const handleToggle = (taskId: string) => {    startTransition(() => {      addOptimisticTask({ type: 'toggle', id: taskId })      toggleTask(taskId)    })  }  const handleDelete = (taskId: string) => {    startTransition(() => {      addOptimisticTask({ type: 'delete', id: taskId })      deleteTask(taskId)    })  }  const getPriorityColor = (priority: string) => {    switch (priority) {      case 'URGENT': return 'text-red-600'      case 'HIGH': return 'text-orange-600'      case 'MEDIUM': return 'text-yellow-600'      case 'LOW': return 'text-green-600'      default: return 'text-gray-600'    }  }  const getPriorityBorder = (priority: string) => {    switch (priority) {      case 'URGENT': return 'border-l-red-500'      case 'HIGH': return 'border-l-orange-500'      case 'MEDIUM': return 'border-l-yellow-500'      case 'LOW': return 'border-l-green-500'      default: return 'border-l-gray-500'    }  }  if (optimisticTasks.length === 0) {    return (      <div className="text-center py-12 text-gray-500">        <p className="text-lg">No tasks yet!</p>        <p className="text-sm">Create your first task to get started.</p>      </div>    )  }  return (    <div className="space-y-3">      {optimisticTasks.map((task) => (        <div          key={task.id}          className={`relative p-4 bg-white dark:bg-gray-800 rounded-lg border-l-4 shadow-sm transition-all duration-200 ${            getPriorityBorder(task.priority)          } ${            task.completed ? 'opacity-75' : ''          } ${            isPending ? 'opacity-50 pointer-events-none' : ''          }`}        >          <div className="flex items-start gap-3">            <Checkbox              checked={task.completed}              onCheckedChange={() => handleToggle(task.id)}              className="mt-1"            />                        <div className="flex-1 min-w-0">              <h3 className={`font-medium ${                task.completed                   ? 'line-through text-gray-500 dark:text-gray-400'                   : 'text-gray-900 dark:text-white'              }`}>                {task.title}              </h3>                            {task.description && (                <p className={`text-sm mt-1 ${                  task.completed                     ? 'line-through text-gray-400 dark:text-gray-500'                     : 'text-gray-600 dark:text-gray-300'                }`}>                  {task.description}                </p>              )}                            <div className="flex items-center gap-4 mt-2 text-xs text-gray-500 dark:text-gray-400">                <div className={`flex items-center gap-1 ${getPriorityColor(task.priority)}`}>                  <Flag className="w-3 h-3" />                  <span className="capitalize">{task.priority.toLowerCase()}</span>                </div>                                {task.dueDate && (                  <div className="flex items-center gap-1">                    <Calendar className="w-3 h-3" />                    <span>{format(new Date(task.dueDate), 'MMM d, yyyy')}</span>                  </div>                )}                                <span>                  Created {format(new Date(task.createdAt), 'MMM d')}                </span>              </div>            </div>                        <Button              variant="ghost"              size="sm"              onClick={() => handleDelete(task.id)}              className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20"            >              <Trash2 className="w-4 h-4" />            </Button>          </div>        </div>      ))}            {isPending && (        <div className="text-center py-2 text-sm text-gray-500">          Saving changes...        </div>      )}    </div>  )}

React 19 useOptimistic Hook Demo

Experience instant UI updates with optimistic mutations! Try adding, toggling, and deleting todos.

import { useState, useOptimistic, useTransition } from 'react';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
  createdAt: string;
  isOptimistic?: boolean;
}

type OptimisticAction = 
  | { type: 'add'; tempId: number; text: string }
  | { type: 'toggle'; id: number }
  | { type: 'delete'; id: number };

// Simulate async operations
const delay = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));

const mockApi = {
  async addTodo(text: string): Promise<Todo> {
    await delay(1000);
    return {
      id: Date.now(),
      text,
      completed: false,
      createdAt: new Date().toISOString()
    };
  },
  
  async toggleTodo(id: number): Promise<{ success: boolean }> {
    await delay(500);
    return { success: true };
  },
  
  async deleteTodo(id: number): Promise<{ success: boolean }> {
    await delay(500);
    return { success: true };
  }
};

export default function OptimisticTodoApp(): JSX.Element {
  const [todos, setTodos] = useState<Todo[]>([
    { id: 1, text: 'Try the optimistic updates', completed: false, createdAt: '2024-01-01' },
    { id: 2, text: 'Notice instant UI feedback', completed: true, createdAt: '2024-01-01' }
  ]);
  
  const [isPending, startTransition] = useTransition();
  const [newTodoText, setNewTodoText] = useState<string>('');
  
  // Optimistic state management
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state: Todo[], action: OptimisticAction): Todo[] => {
      switch (action.type) {
        case 'add':
          return [...state, {
            id: action.tempId,
            text: action.text,
            completed: false,
            createdAt: new Date().toISOString(),
            isOptimistic: true
          }];
          
        case 'toggle':
          return state.map(todo =>
            todo.id === action.id
              ? { ...todo, completed: !todo.completed }
              : todo
          );
          
        case 'delete':
          return state.filter(todo => todo.id !== action.id);
          
        default:
          return state;
      }
    }
  );
  
  const handleAddTodo = async () => {
    if (!newTodoText.trim()) return;
    
    const tempId = Date.now();
    const text = newTodoText;
    setNewTodoText('');
    
    startTransition(async () => {
      // Optimistic update
      addOptimisticTodo({ type: 'add', tempId, text });
      
      try {
        // Real API call
        const newTodo = await mockApi.addTodo(text);
        setTodos(prev => [...prev, newTodo]);
      } catch (error) {
        // In real app, you'd handle rollback here
        console.error('Failed to add todo:', error);
      }
    });
  };
  
  const handleToggle = (id: number): void => {
    startTransition(async () => {
      // Optimistic update
      addOptimisticTodo({ type: 'toggle', id });
      
      try {
        await mockApi.toggleTodo(id);
        setTodos(prev => prev.map(todo =>
          todo.id === id ? { ...todo, completed: !todo.completed } : todo
        ));
      } catch (error) {
        console.error('Failed to toggle todo:', error);
      }
    });
  };
  
  const handleDelete = (id: number): void => {
    startTransition(async () => {
      // Optimistic update
      addOptimisticTodo({ type: 'delete', id });
      
      try {
        await mockApi.deleteTodo(id);
        setTodos(prev => prev.filter(todo => todo.id !== id));
      } catch (error) {
        console.error('Failed to delete todo:', error);
      }
    });
  };
  
  return (
    <div style={{
      maxWidth: '500px',
      margin: '20px auto',
      padding: '20px',
      fontFamily: 'system-ui, sans-serif',
      backgroundColor: '#f9fafb',
      borderRadius: '12px',
      border: '1px solid #e5e7eb'
    }}>
      <h2 style={{ 
        marginBottom: '24px', 
        color: '#1f2937',
        textAlign: 'center' 
      }}>
        🚀 Optimistic UI Demo
      </h2>
      
      {/* Add Todo Form */}
      <div style={{ 
        display: 'flex', 
        gap: '10px', 
        marginBottom: '20px' 
      }}>
        <input
          type="text"
          value={newTodoText}
          onChange={(e) => setNewTodoText(e.target.value)}
          placeholder="Add a new todo..."
          onKeyPress={(e) => e.key === 'Enter' && handleAddTodo()}
          style={{
            flex: 1,
            padding: '12px',
            border: '2px solid #e5e7eb',
            borderRadius: '8px',
            fontSize: '16px',
            outline: 'none'
          }}
        />
        <button
          onClick={handleAddTodo}
          disabled={!newTodoText.trim()}
          style={{
            padding: '12px 20px',
            backgroundColor: newTodoText.trim() ? '#3b82f6' : '#9ca3af',
            color: 'white',
            border: 'none',
            borderRadius: '8px',
            cursor: newTodoText.trim() ? 'pointer' : 'not-allowed',
            fontSize: '16px',
            fontWeight: '500'
          }}
        >
          Add
        </button>
      </div>
      
      {/* Loading Indicator */}
      {isPending && (
        <div style={{
          textAlign: 'center',
          padding: '10px',
          backgroundColor: '#dbeafe',
          border: '1px solid #93c5fd',
          borderRadius: '6px',
          marginBottom: '16px',
          color: '#1e40af'
        }}>
          ⚡ Processing... (Notice the instant UI updates!)
        </div>
      )}
      
      {/* Todo List */}
      <div style={{ space: '8px' }}>
        {optimisticTodos.map((todo) => (
          <div
            key={todo.id}
            style={{
              display: 'flex',
              alignItems: 'center',
              gap: '12px',
              padding: '16px',
              backgroundColor: 'white',
              border: '1px solid #e5e7eb',
              borderRadius: '8px',
              marginBottom: '8px',
              opacity: todo.isOptimistic ? 0.7 : 1,
              transition: 'opacity 0.2s'
            }}
          >
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => handleToggle(todo.id)}
              style={{ 
                width: '18px', 
                height: '18px',
                cursor: 'pointer'
              }}
            />
            
            <span
              style={{
                flex: 1,
                textDecoration: todo.completed ? 'line-through' : 'none',
                color: todo.completed ? '#6b7280' : '#1f2937',
                fontSize: '16px'
              }}
            >
              {todo.text}
            </span>
            
            {todo.isOptimistic && (
              <span style={{
                fontSize: '12px',
                color: '#3b82f6',
                backgroundColor: '#dbeafe',
                padding: '2px 6px',
                borderRadius: '4px'
              }}>
                Saving...
              </span>
            )}
            
            <button
              onClick={() => handleDelete(todo.id)}
              style={{
                padding: '6px 12px',
                backgroundColor: '#ef4444',
                color: 'white',
                border: 'none',
                borderRadius: '6px',
                cursor: 'pointer',
                fontSize: '14px'
              }}
            >
              Delete
            </button>
          </div>
        ))}
        
        {optimisticTodos.length === 0 && (
          <div style={{
            textAlign: 'center',
            padding: '40px',
            color: '#6b7280',
            fontStyle: 'italic'
          }}>
            No todos yet. Add one above!
          </div>
        )}
      </div>
      
      <div style={{
        marginTop: '16px',
        padding: '12px',
        backgroundColor: '#f3f4f6',
        borderRadius: '6px',
        fontSize: '14px',
        color: '#4b5563'
      }}>
        💡 <strong>Try it:</strong> Add, toggle, or delete todos. Notice how the UI updates instantly 
        even though there's a simulated network delay!
      </div>
    </div>
  );
}

Phase 5: Production Deployment

Deploy your application to production with Vercel, set up a production database, and configure environment variables for a live application.

Step 12: Prepare for Production

Terminal - Build Optimization
# Type check the entire projectnpm run type-check# Lint and fix issuesnpm run lint# Test production build locallynpm run buildnpm run start# Analyze bundle sizenpm run analyze

Step 13: Deploy to Vercel

Terminal - Vercel Deployment
# Install Vercel CLInpm i -g vercel# Login to Vercelvercel login# Deploy to productionvercel --prod# Or connect GitHub repository for automatic deployments# 1. Push code to GitHub# 2. Import project in Vercel dashboard# 3. Configure environment variables# 4. Deploy automatically on every push

Step 14: Production Database Setup

Set up a production PostgreSQL database with Vercel Postgres or your preferred provider:

Terminal - Vercel Postgres
# Create Vercel Postgres databasevercel storage create postgres --type postgres# Get connection stringvercel env pull .env.production# Run database migration in productionnpx prisma db push --schema=./prisma/schema.prisma

Step 15: Environment Variables for Production

🔒

Production Environment Variables

DATABASE_URL - Production PostgreSQL connection string
BETTER_AUTH_SECRET - Strong secret for JWT signing (use: openssl rand -base64 32)
BETTER_AUTH_URL - Your production domain (https://yourdomain.com)
GOOGLE_CLIENT_ID & GOOGLE_CLIENT_SECRET - OAuth credentials
GITHUB_CLIENT_ID & GITHUB_CLIENT_SECRET - OAuth credentials
NODE_ENV=production - Ensure production optimizations
Vercel Environment Variables
# Set environment variables in Vercel dashboard or CLIvercel env add DATABASE_URLvercel env add BETTER_AUTH_SECRETvercel env add BETTER_AUTH_URLvercel env add GOOGLE_CLIENT_IDvercel env add GOOGLE_CLIENT_SECRETvercel env add GITHUB_CLIENT_IDvercel env add GITHUB_CLIENT_SECRET# Deploy with new environment variablesvercel --prod

Performance Optimizations and Best Practices

Implement advanced performance optimizations and monitoring for your production application:

Caching Strategy

Implement intelligent caching with Next.js 15 fetch configurations

Error Monitoring

Set up error tracking and performance monitoring

SEO Optimization

Configure metadata, sitemaps, and search engine optimization

Security Hardening

Implement security headers, CSRF protection, and rate limiting

src/lib/cache.ts - Advanced Caching
// Next.js 15 explicit caching strategiesexport async function getTasks(userId: string) {  const response = await fetch(`${process.env.NEXTAUTH_URL}/api/tasks?userId=${userId}`, {    // Cache for 5 minutes with background revalidation    next: {       revalidate: 300,      tags: [`tasks-${userId}`]    },    cache: 'force-cache'  })    return response.json()}// Cache invalidation on mutationsimport { revalidateTag } from 'next/cache'export async function invalidateUserTasks(userId: string) {  revalidateTag(`tasks-${userId}`)}

Congratulations! You've built a complete, production-ready task management application with Next.js 15. Your app includes modern React 19 hooks, optimistic UI updates, secure authentication, database integration, and is deployed to production with proper caching and performance optimizations.

Next Steps and Advanced Features

Take your application further with these advanced features:

  • Real-time collaboration with WebSockets or Server-Sent Events
  • Advanced filtering and search with full-text search
  • File attachments and image uploads with Vercel Blob
  • Mobile app with React Native and shared API
  • Team collaboration features with role-based permissions
  • Advanced analytics and user behavior tracking
  • Progressive Web App (PWA) capabilities
  • Internationalization (i18n) for global users

Interactive Learning Summary

Throughout this guide, you've experienced hands-on learning with three interactive code playgrounds that demonstrated key Next.js 15 and React 19 concepts:

useActionState Hook Demo

Interactive form handling with validation, loading states, and error management using React 19's newest hook

Server Actions Playground

Full-stack book manager showcasing Next.js Server Actions, progressive enhancement, and multi-file component architecture

useOptimistic Hook Demo

Todo app demonstrating instant UI updates, optimistic mutations, and graceful error handling with visual feedback

These interactive examples provided immediate feedback as you learned, allowing you to experiment with modern React patterns and see how they work in real-time. This hands-on approach reinforces theoretical concepts with practical application.

You now have a solid foundation in Next.js 15 and modern React development. The patterns and techniques you've learned can be applied to any full-stack application. Keep building, keep learning, and keep pushing the boundaries of what's possible with modern web development!

Final Challenge: Build Your Own TaskFlow

Ready to put everything together? Here's a complete mini TaskFlow application that combines all the concepts we've covered. Try modifying it to add your own features!

Complete TaskFlow Mini-App

A fully functional task management app combining all Next.js 15 and React 19 concepts from this guide. Try adding new features!

import { useState, useOptimistic, useTransition, useActionState } from 'react';

interface Task {
  id: number;
  title: string;
  priority: 'low' | 'medium' | 'high';
  completed: boolean;
  createdAt: string;
  isOptimistic?: boolean;
}

interface TaskFormState {
  success: boolean;
  error: string | null;
}

type OptimisticAction = 
  | { type: 'add'; task: Task }
  | { type: 'toggle'; id: number }
  | { type: 'delete'; id: number };

// Mock API functions (simulate server actions)
const mockApi = {
  async createTask(formData: FormData): Promise<Task> {
    await new Promise(resolve => setTimeout(resolve, 800));
    const title = formData.get('title') as string;
    const priority = (formData.get('priority') as Task['priority']) || 'medium';
    
    if (!title || title.trim().length < 3) {
      throw new Error('Task title must be at least 3 characters');
    }
    
    return {
      id: Date.now(),
      title: title.trim(),
      priority,
      completed: false,
      createdAt: new Date().toISOString()
    };
  },
  
  async toggleTask(id: number): Promise<{ success: boolean }> {
    await new Promise(resolve => setTimeout(resolve, 400));
    return { success: true };
  },
  
  async deleteTask(id: number): Promise<{ success: boolean }> {
    await new Promise(resolve => setTimeout(resolve, 600));
    return { success: true };
  }
};

// Task Form Component with useActionState
interface TaskFormProps {
  onTaskAdded: (task: Task) => void;
}

function TaskForm({ onTaskAdded }: TaskFormProps): JSX.Element {
  const [isPending, startTransition] = useTransition();
  
  const submitTask = async (prevState: TaskFormState, formData: FormData): Promise<TaskFormState> => {
    try {
      const newTask = await mockApi.createTask(formData);
      onTaskAdded(newTask);
      return { success: true, error: null };
    } catch (error) {
      return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
    }
  };
  
  const [state, formAction] = useActionState(submitTask, { success: false, error: null });
  
  return (
    <div style={{ 
      backgroundColor: '#f8fafc', 
      padding: '20px', 
      borderRadius: '12px',
      border: '1px solid #e2e8f0',
      marginBottom: '24px'
    }}>
      <h3 style={{ 
        margin: '0 0 16px 0', 
        color: '#1e293b',
        fontSize: '18px',
        fontWeight: '600'
      }}>
        ✨ Add New Task
      </h3>
      
      <form action={formAction} style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
        <input
          name="title"
          type="text"
          placeholder="What needs to be done?"
          style={{
            padding: '12px',
            border: '2px solid #e2e8f0',
            borderRadius: '8px',
            fontSize: '16px',
            outline: 'none',
            transition: 'border-color 0.2s'
          }}
          onFocus={(e) => e.target.style.borderColor = '#3b82f6'}
          onBlur={(e) => e.target.style.borderColor = '#e2e8f0'}
        />
        
        <select
          name="priority"
          style={{
            padding: '12px',
            border: '2px solid #e2e8f0',
            borderRadius: '8px',
            fontSize: '16px',
            outline: 'none'
          }}
        >
          <option value="low">🟢 Low Priority</option>
          <option value="medium">🟡 Medium Priority</option>
          <option value="high">🔴 High Priority</option>
        </select>
        
        <button
          type="submit"
          disabled={isPending}
          style={{
            padding: '12px 20px',
            backgroundColor: isPending ? '#94a3b8' : '#3b82f6',
            color: 'white',
            border: 'none',
            borderRadius: '8px',
            fontSize: '16px',
            fontWeight: '500',
            cursor: isPending ? 'not-allowed' : 'pointer',
            transition: 'all 0.2s'
          }}
        >
          {isPending ? '⏳ Adding Task...' : '➕ Add Task'}
        </button>
        
        {state.error && (
          <div style={{
            padding: '12px',
            backgroundColor: '#fee2e2',
            border: '1px solid #fecaca',
            borderRadius: '6px',
            color: '#dc2626',
            fontSize: '14px'
          }}>{state.error}
          </div>
        )}
      </form>
    </div>
  );
}

// Task Item Component
interface TaskItemProps {
  task: Task;
  onToggle: (id: number) => void;
  onDelete: (id: number) => void;
  isOptimistic?: boolean;
}

function TaskItem({ task, onToggle, onDelete, isOptimistic }: TaskItemProps): JSX.Element {
  const getPriorityColor = (priority: Task['priority']): string => {
    switch (priority) {
      case 'high': return '#ef4444';
      case 'medium': return '#f59e0b';
      case 'low': return '#10b981';
      default: return '#6b7280';
    }
  };
  
  return (
    <div style={{
      display: 'flex',
      alignItems: 'center',
      gap: '12px',
      padding: '16px',
      backgroundColor: 'white',
      border: '1px solid #e5e7eb',
      borderRadius: '8px',
      marginBottom: '8px',
      opacity: isOptimistic ? 0.7 : 1,
      transition: 'all 0.3s',
      boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)'
    }}>
      <input
        type="checkbox"
        checked={task.completed}
        onChange={() => onToggle(task.id)}
        style={{ 
          width: '18px', 
          height: '18px',
          cursor: 'pointer',
          accentColor: '#3b82f6'
        }}
      />
      
      <div style={{
        width: '8px',
        height: '8px',
        borderRadius: '50%',
        backgroundColor: getPriorityColor(task.priority),
        flexShrink: 0
      }} />
      
      <div style={{ flex: 1 }}>
        <div style={{
          textDecoration: task.completed ? 'line-through' : 'none',
          color: task.completed ? '#6b7280' : '#1f2937',
          fontSize: '16px',
          fontWeight: '500'
        }}>
          {task.title}
        </div>
        <div style={{
          fontSize: '12px',
          color: '#6b7280',
          marginTop: '2px'
        }}>
          {task.priority.toUpperCase()} • Created {new Date(task.createdAt).toLocaleString()}
        </div>
      </div>
      
      {isOptimistic && (
        <span style={{
          fontSize: '12px',
          color: '#3b82f6',
          backgroundColor: '#dbeafe',
          padding: '4px 8px',
          borderRadius: '4px'
        }}>
          Saving...
        </span>
      )}
      
      <button
        onClick={() => onDelete(task.id)}
        style={{
          padding: '8px',
          backgroundColor: '#fef2f2',
          color: '#dc2626',
          border: '1px solid #fecaca',
          borderRadius: '6px',
          cursor: 'pointer',
          fontSize: '14px',
          transition: 'all 0.2s'
        }}
        onMouseEnter={(e) => {
          e.target.style.backgroundColor = '#fee2e2';
        }}
        onMouseLeave={(e) => {
          e.target.style.backgroundColor = '#fef2f2';
        }}
      >
        🗑️
      </button>
    </div>
  );
}

// Main TaskFlow App
export default function TaskFlowApp(): JSX.Element {
  const [tasks, setTasks] = useState<Task[]>([
    {
      id: 1,
      title: 'Learn Next.js 15 App Router',
      priority: 'high',
      completed: true,
      createdAt: new Date(Date.now() - 86400000).toISOString()
    },
    {
      id: 2,
      title: 'Master React 19 Hooks',
      priority: 'medium',
      completed: false,
      createdAt: new Date(Date.now() - 43200000).toISOString()
    },
    {
      id: 3,
      title: 'Deploy to Production',
      priority: 'low',
      completed: false,
      createdAt: new Date().toISOString()
    }
  ]);
  
  const [isPending, startTransition] = useTransition();
  
  // Optimistic updates with useOptimistic
  const [optimisticTasks, addOptimisticTask] = useOptimistic(
    tasks,
    (state: Task[], action: OptimisticAction): Task[] => {
      switch (action.type) {
        case 'add':
          return [...state, { ...action.task, isOptimistic: true }];
        case 'toggle':
          return state.map(task =>
            task.id === action.id 
              ? { ...task, completed: !task.completed, isOptimistic: true }
              : task
          );
        case 'delete':
          return state.filter(task => task.id !== action.id);
        default:
          return state;
      }
    }
  );
  
  const handleTaskAdded = (newTask: Task): void => {
    startTransition(() => {
      addOptimisticTask({ type: 'add', task: newTask });
      setTasks(prev => [...prev, newTask]);
    });
  };
  
  const handleToggle = (id: number): void => {
    startTransition(async () => {
      addOptimisticTask({ type: 'toggle', id });
      try {
        await mockApi.toggleTask(id);
        setTasks(prev => prev.map(task =>
          task.id === id ? { ...task, completed: !task.completed } : task
        ));
      } catch (error) {
        console.error('Failed to toggle task:', error);
      }
    });
  };
  
  const handleDelete = (id: number): void => {
    startTransition(async () => {
      addOptimisticTask({ type: 'delete', id });
      try {
        await mockApi.deleteTask(id);
        setTasks(prev => prev.filter(task => task.id !== id));
      } catch (error) {
        console.error('Failed to delete task:', error);
      }
    });
  };
  
  const completedCount = optimisticTasks.filter(task => task.completed).length;
  const totalCount = optimisticTasks.length;
  
  return (
    <div style={{ 
      maxWidth: '600px', 
      margin: '20px auto', 
      padding: '24px',
      fontFamily: 'system-ui, -apple-system, sans-serif',
      backgroundColor: '#ffffff',
      minHeight: '100vh'
    }}>
      {/* Header */}
      <div style={{ 
        textAlign: 'center', 
        marginBottom: '32px',
        padding: '24px',
        background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
        borderRadius: '16px',
        color: 'white'
      }}>
        <h1 style={{ 
          margin: '0 0 8px 0', 
          fontSize: '32px',
          fontWeight: '700'
        }}>
          🚀 TaskFlow
        </h1>
        <p style={{ 
          margin: 0, 
          fontSize: '18px',
          opacity: 0.9
        }}>
          Next.js 15 + React 19 Task Manager
        </p>
        <div style={{
          marginTop: '12px',
          fontSize: '16px',
          fontWeight: '500'
        }}>
          {completedCount} of {totalCount} tasks completed
        </div>
      </div>
      
      {/* Add Task Form */}
      <TaskForm onTaskAdded={handleTaskAdded} />
      
      {/* Loading State */}
      {isPending && (
        <div style={{
          textAlign: 'center',
          padding: '12px',
          backgroundColor: '#dbeafe',
          border: '1px solid #93c5fd',
          borderRadius: '8px',
          marginBottom: '16px',
          color: '#1e40af'
        }}>
          ⚡ Processing your request...
        </div>
      )}
      
      {/* Task List */}
      <div style={{ marginBottom: '24px' }}>
        <h3 style={{ 
          margin: '0 0 16px 0',
          color: '#1e293b',
          fontSize: '20px',
          fontWeight: '600'
        }}>
          📋 Your Tasks ({totalCount})
        </h3>
        
        {optimisticTasks.length === 0 ? (
          <div style={{
            textAlign: 'center',
            padding: '48px',
            backgroundColor: '#f8fafc',
            borderRadius: '12px',
            color: '#64748b',
            fontSize: '16px'
          }}>
            🎉 No tasks yet! Add one above to get started.
          </div>
        ) : (
          <div>
            {optimisticTasks.map((task) => (
              <TaskItem
                key={task.id}
                task={task}
                onToggle={handleToggle}
                onDelete={handleDelete}
                isOptimistic={task.isOptimistic}
              />
            ))}
          </div>
        )}
      </div>
      
      {/* Footer */}
      <div style={{
        padding: '20px',
        backgroundColor: '#f1f5f9',
        borderRadius: '12px',
        textAlign: 'center',
        color: '#475569',
        fontSize: '14px'
      }}>
        <div style={{ fontWeight: '600', marginBottom: '8px' }}>
          🎯 Features Demonstrated:
        </div>
        <div>
          ✅ useActionState • ✅ useOptimistic • ✅ Server Actions • ✅ Optimistic UI • ✅ Form Handling
        </div>
        <div style={{ marginTop: '8px', fontStyle: 'italic' }}>
          Try modifying the code to add your own features!
        </div>
      </div>
    </div>
  );
}