Flutter Platform Aware Widgets
- Development
Otavio Barros Otavio Barros
December 12, 2024 • 12 min read
Choosing the right web framework can significantly impact your project's success. Next.js has emerged as a powerhouse in the React ecosystem, offering a robust solution for building modern web applications. As we explore Next.js 15, let's understand why developers and businesses increasingly rely on this framework.
In this post we'll explore significant improvements in Next’s routing, server components, caching behavior, and performance optimizations. These updates continue to reshape how we approach modern web development, making it easier to build faster, more reliable applications.
Let's start by examining the revolutionary App Router, followed by deep dives into server components, caching strategies, and solutions to common performance challenges.
Next.js 15's App Router represents a new approach to application structure, building upon the stable foundation introduced in version 13. At its core, the App Router uses a file-system based routing that transforms how we organize and build web applications.
The foundation of any Next.js application starts with a root layout:
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<header>Global Navigation</header>
<main>{children}</main>
<footer>Global Footer</footer>
</body>
</html>
);
}
This root layout wraps every page in your application, providing consistent UI elements across all routes.
The App Router follows a predictable folder structure where each folder represents a route segment:
app/
├── layout.tsx
├── page.tsx
├── dashboard/
│ ├── layout.tsx
│ ├── page.tsx
│ └── settings/
│ └── page.tsx
└── blog/
├── layout.tsx
└── [slug]/
└── page.tsx
Each page.tsx file automatically becomes a route, while layout.tsx files create a shared UI for their respective segments.
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard-container">
<aside>
<nav>Dashboard Navigation</nav>
</aside>
<section className="content">{children}</section>
</div>
);
}
This layout will wrap all pages within the dashboard section while preserving its state during navigation between dashboard routes.
The App Router introduces special files that handle common scenarios:
loading.tsx for suspense boundarieserror.tsx for error handlingnot-found.tsx for 404 casespage.tsx for route UI// app/dashboard/loading.tsx
export default function Loading() {
return <div className="dashboard-loader">Loading...</div>;
}
These files automatically create optimal loading and error states for your application.
The App Router's architecture naturally leads us to Server Components, which form the backbone of Next.js 15's performance optimizations. These components, rendered entirely on the server, work seamlessly with the App Router to deliver exceptional performance and developer experience.
Next.js 15 enhances both Server Components and Server Actions, providing developers with powerful tools for building efficient, secure applications. Let's explore these features and their practical applications.
Server Components
Server Components fundamentally change how we build React applications by moving component rendering to the server. Here's a practical example:
// app/products/page.tsx
async function ProductList() {
const products = await fetchProducts(); // Server-side data fetching
return (
<div className="grid">
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
// Sensitive data can be safely passed
internalMetrics={product.metrics}
/>
))}
</div>
);
}
Key Benefits:
Streaming with Suspense:
// app/dashboard/page.tsx
import { Suspense } from "react";
export default function Dashboard() {
return (
<div>
<Suspense fallback={<UserProfileSkeleton />}>
<UserProfile />
</Suspense>
<Suspense fallback={<AnalyticsSkeleton />}>
<AnalyticsChart />
</Suspense>
</div>
);
}
Parallel Data Fetching:
// app/profile/page.tsx
async function ProfilePage() {
// These requests run in parallel
const [userData, userPosts, userAnalytics] = await Promise.all([
fetchUserData(),
fetchUserPosts(),
fetchUserAnalytics(),
]);
return (
<div>
<UserInfo data={userData} />
<UserPosts posts={userPosts} />
<UserStats analytics={userAnalytics} />
</div>
);
}
Server Actions provide a secure way to handle server-side mutations. Next.js 15 introduces enhanced security features and better integration with forms.
Basic Form Handling:
// app/actions.ts
"use server";
export async function updateProfile(formData: FormData) {
const name = formData.get("name");
const email = formData.get("email");
await db.user.update({
where: { id: session.user.id },
data: { name, email },
});
}
// app/profile/page.tsx
export default function ProfileForm() {
return (
<form action={updateProfile}>
<input name="name" type="text" />
<input name="email" type="email" />
<button type="submit">Update Profile</button>
</form>
);
}
Advanced Server Action Pattern:
// app/actions.ts
"use server";
export async function handleOrder(formData: FormData) {
const items = JSON.parse(formData.get("items") as string);
try {
// Start transaction
const order = await db.$transaction(async (tx) => {
// Check inventory
await validateInventory(items);
// Process payment
const payment = await processPayment(items);
// Create order
return await createOrder(items, payment);
});
revalidatePath("/orders");
return { success: true, orderId: order.id };
} catch (error) {
return { success: false, error: error.message };
}
}
Client-Side Integration:
"use client";
import { handleOrder } from "./actions";
import { useTransition } from "react";
export function CheckoutButton({ items }) {
const [isPending, startTransition] = useTransition();
const processCheckout = () => {
startTransition(async () => {
const formData = new FormData();
formData.append("items", JSON.stringify(items));
const result = await handleOrder(formData);
if (result.success) {
// Handle success
}
});
};
return (
<button onClick={processCheckout} disabled={isPending}>
{isPending ? "Processing..." : "Checkout"}
</button>
);
}
Server Components and Actions in Next.js 15 provide a robust foundation for building modern web applications. They enable developers to create performant, secure, and maintainable applications while keeping sensitive operations server-side. The next section will explore how Next.js 15 handles caching to further optimize application performance.
Next.js 15 introduces significant changes to its caching behavior, moving from an opinionated cached-by-default approach to giving developers more explicit control over caching strategies.
In version 15, requests are no longer cached by default. This provides more predictable behavior and better control over data freshness. Here's how to implement different caching strategies:
Time-based Caching
// Basic fetch with cache
async function getProducts() {
const products = await fetch("https://api.store.com/products", {
cache: "force-cache",
next: { revalidate: 3600 }, // Revalidate every hour
});
return products.json();
}
// Using route segment config
export const revalidate = 3600; // Page-level revalidation
export default async function ProductsPage() {
const products = await getProducts();
return (
<div className="products-grid">
{products.map((product) => (
<ProductCard key={product.id} {...product} />
))}
</div>
);
}
On-Demand Revalidation:
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
export async function updateProduct(formData: FormData) {
await saveProduct(formData);
// Revalidate the products page and all its subpages
revalidatePath("/products");
}
Using Tag-based Revalidation:
// Fetching with tags
async function getProduct(id: string) {
const product = await fetch(`https://api.store.com/products/${id}`, {
next: { tags: ["products"] },
});
return product.json();
}
// Revalidating tagged data
export async function updateProducts() {
"use server";
await updateProductInDatabase();
revalidateTag("products");
}
Next.js automatically deduplicates identical requests during the same render pass. Here's how to leverage this:
// This component can be used multiple times, but only one request will be made
async function ProductInfo({ id }: { id: string }) {
const product = await fetch(`https://api.store.com/products/${id}`, {
cache: "force-cache",
});
return product.json();
}
// Multiple instances will trigger only one request
export default async function Page() {
return (
<>
<ProductInfo id="1" />
<ProductInfo id="1" /> {/* Reuses cached data */}
<OtherComponent>
<ProductInfo id="1" /> {/* Still reuses cached data */}
</OtherComponent>
</>
);
}
// Combining multiple caching strategies
async function getDashboardData() {
const [
products = await fetch("/api/products", { cache: "force-cache" }),
recentOrders = await fetch("/api/orders/recent", { cache: "no-store" }),
analytics = await fetch("/api/analytics", { next: { revalidate: 300 } }),
] = await Promise.all([getProducts(), getRecentOrders(), getAnalytics()]);
return {
products: await products.json(),
recentOrders: await recentOrders.json(),
analytics: await analytics.json(),
};
}
This caching system in Next.js 15 provides developers with fine-grained control over data freshness while maintaining optimal performance through automatic request deduplication and efficient cache invalidation strategies.
Next.js 15 introduces several significant improvements that enhance both development experience and application performance.
The framework now fully supports React 19 RC, bringing access to new React features and improved hydration error handling. Hydration errors now display detailed source code information with actionable suggestions for resolution.
Turbopack has reached stability in development mode, delivering impressive performance gains:
Server Actions received significant security improvements:
Development Experience
next.config.tsexpireTime value in next.configThese updates collectively make Next.js 15 a more robust, secure, and developer-friendly framework.
Next.js 15 marks a significant evolution in modern web development, introducing crucial improvements in routing, server-side rendering, and caching mechanisms. The framework maintains its position as a leading choice for building performant web applications while providing developers with more explicit control over their applications' behavior.
Whether you're building a new project or considering an upgrade, Next.js 15's enhanced features and improved developer experience make it a compelling choice for teams focused on building fast, reliable, and maintainable web applications.
The future of web development continues to evolve, and Next.js remains at the forefront of this evolution, balancing innovation with practical development needs.