Next.js: App Router, Server Components, and More

Next.js: App Router, Server Components, and More

Intro

For quite some time, the goal of web development has been to provide users with extraordinary experiences that match the seamless nature of native mobile applications. To create such experiences, many frameworks have shifted rendering and data fetching to the client. This approach often overlooks the server's potential, sacrificing personalization and performance in favor of interactivity. What if we could have the best of both worlds? This is what Next.js is doing with the introduction of a whole new architecture built on top of React Server Components as a primitive.

In my journey as a developer, I always enjoyed the developer experience provided by Next.js, and with its latest releases, they seem to be taking things to a whole new level. The Next.js App router, which has been stable and ready for production for some months now, takes advantage of a straightforward programming model inspired by the strengths of PHP and Ruby on Rails combined with the power of React as its interactive UI engine. By leveraging the best aspects of server and client capabilities, Next.js offers developers an optimal solution for building web applications.

Data fetching

As developers, we know that data is the core of every application. One aspect that truly impressed me is how, by constructing on the foundation of React server components, Next.js makes it incredibly simple to fetch data. All we need to know is how to use the async and await syntax from JavaScript. We have to declare an async function to fetch the data, mark our React component as async and await for the data inside the component:

async function getUsers() {
const res = await fetch("https://my-api.com/users");
return res.json();
}
export default async function Page() {
const users = await getUsers();
return (
<>
{users.map((user) => (
<p>{user.name}</p>
))}
</>
);
}

In the App directory, you can fetch data inside layouts, pages, and components. Next.js's granular caching allows you to choose between static and dynamic data at the fetch level. This is thanks to an extension of the native fetch Web API that provides one flexible way to fetch, cache, and revalidate data at the component level.

Static data fetching

Fetch caches the data indefinitely by default. As a result, even if the data from the API changes, refreshing the page won't trigger an update of the data on the site. This caching behavior is suitable for websites that primarily display static data that rarely changes. The example above illustrates this approach.

// Cached until manually invalidated
// 'force-cache' is the default and can be omitted
fetch("https://my-api.com/users", {
cache: "force-cache",
});

Dynamic data fetching

We can tell the fetch API to never cache the data by using "no-store".

// Refetches data on every request
fetch("https://my-api.com/users", {
cache: "no-store",
});

Revalidating data

We can tell Next.js to revalidate the data after a certain amount of time. This will revalidate the data every hour:

// Cached with a lifetime of 1 hour
fetch("https://my-api.com/users", {
next: { revalidate: 3600 },
});

And the best part is that these fetching strategies can be used together within the same route, something that was not possible in previous versions.

Data mutations

Until now, modeling data mutations in React with Next.js proved to be a challenging task. We had to go through a series of steps, including creating an API route, adding an effect to hit the API, and storing the response in state. This approach often led to a lot of client-side JavaScript and relying on third-party libraries.

With the introduction of React's server actions to Next.js, we are able to effortlessly execute server first data mutations. This means even less client side JS and a cleaner codebase since the heavy lifting will be done on the server. Server actions are now stable and ready to use. Here you can see an example in action:

import { cookies } from "next/headers";
// Server action defined inside a Server Component
export default function AddToCart({ productId }) {
async function addItem(data) {
"use server";
const cartId = cookies().get("cartId")?.value;
await saveToDb({ cartId, data });
}
return (
<form action={addItem}>
<button type="submit">Add to Cart</button>
</form>
);
}
Server action in server component

In this example, the server action is created by defining addItem as an asynchronous function with the 'use server' directive at the top of the function body. The function is triggered when the form is submitted (i.e., when the "Add to Cart" button is clicked).

Metadata API

In addition to its many other features, the Next.js App Router has built-in SEO support through the metadata API. This enhancement offers developers a seamless way to customize their web pages for optimal sharing and discoverability. With the metadata API, we can effortlessly define and manage essential metadata elements such as titles, descriptions, keywords, etc.

Static metadata

Here's an example of how you can define static metadata by exporting a metadata object from a layout.tsx or page.tsx file:

export const metadata = {
title: "Travel Destinations",
description: "Explore the best travel destinations around the world",
keywords: "travel, destinations, tourism, vacation",
};

Dynamic metadata

Sometimes you need to generate metadata based on dynamic information, such as the current route parameters, external data, or metadata in parent segments. To implement dynamic metadata, you can export a generateMetadata function that returns a metadata object from a layout.tsx or page.tsx file.

Let's consider an e-commerce application. In the app/products/[id]/page.tsx, you can define a generateMetadata function to create custom metadata for each product page, dynamically setting the title to match the specific product being displayed:

import { Metadata } from "next";
type Props = {
params: { id: string },
};
export const generateMetadata = async ({
params,
}: Props): Promise<Metadata> => {
const product = await fetchProductById(params.id);
return {
title: product.title,
};
};

Conclusion

In summary, Next.js stands out as a powerful and developer-friendly framework that is revolutionizing the way in which we build full-stack applications. It might take a bit to fully grasp its new capabilities, but the results are truly impressive. The introduction of server components and server actions is a real game changer, opening up incredible possibilities. We are excited for what the future holds and can't wait to see all the creative ideas that will come up in the ecosystem, leveraging the power of React and Next.js innovations.

This brief overview only touches on a few of the features that Next.js has to offer. To dive deeper and unlock the full potential of the framework, head over to the official documentation. There, you'll find helpful guides, including the recently released official Next.js course, and plenty of information to make the most of Next.js in your web development projects. Happy coding!