DEV Community

Cover image for The Ultimate Guide to Server Components, Server Actions, Route Handlers, and Suspense in Next.js App Router
Waffen Sultan
Waffen Sultan

Posted on

2

The Ultimate Guide to Server Components, Server Actions, Route Handlers, and Suspense in Next.js App Router

Overview

With the introduction of Next.js App Router, developers have gained access to powerful new tools for interacting with data—Server Components, Server Actions, and Route Handlers. Each of these runs and executes on the server, ensuring that sensitive information and data remain secure from malicious actors.

How we should interact with data

Next.js recommends that data fetching should happen on Server Components. We can pass down that data to Client Components via props.

Server Actions are not intended for data fetching but for mutations. They handle POST operations.

Route Handlers can serve as an alternative for fetching data if Server Components are not an option. They can also be utilized for integrating with external services. Keep in mind that if you choose to host or deploy on Vercel, Route Handlers may lead to extra usage and costs.

Server Components

Server Components are pretty neat. We should treat Server Components as our primary go-to for when we want to fetch data. Not only do they improve SEO (Search Engine Optimization) and security by running code on the server, they also allow us to utilize Streaming, which is a data transfer technique that gives us the freedom to break down a page into separate "chunks" and progressively stream data to the client as soon as they become available.

An example of how we would typically fetch data with a Server Component:

app/page.tsx

export interface User {
    id: string;
    email: string;
    username: string;
}

/* Fetching data with a server component*/
export default async function Page() {
    const res = await fetch("https://examplebackend.com/users");
    const users = await res.json();

    return (
        <div>
            <ul>
                {users.map((user: User) => (
                    <li key={user.id}>{user.username}</li>
                ))}
            </ul>
        </div>
    );
}

Enter fullscreen mode Exit fullscreen mode

Here is another example of fetching data with Server Components. This time, we are passing down data to a Client Component. Remember that for us to be able to do this, the data we pass down must be serializable.

app/page.tsx

import UserDashboard from "@/app/user-dashboard";

export interface User {
    id: string;
    email: string;
    username: string;
}

/* Fetching data with a server component and passing the data down via props*/
export default async function Page() {
    const res = await fetch("https://examplebackend.com/users");
    const users: User[] = await res.json();

    return <UserDashboard users={users} />;
}

Enter fullscreen mode Exit fullscreen mode

app/user-dashboard.tsx

"use client";

import type { User } from "@/app/page";

import { useState } from "react";

export default function UserDashboard({ users }: { users: User[] }) {
    const [bannedUsers, setBannedUsers] = useState<User[]>([]);

    return (
        <div>
            <ul>
                {users.map((user) => (
                    <li key={user.id}>
                        {user.email} {user.username}
                        <button
                            onClick={() =>
                                setBannedUsers((prev) => [...prev, user])
                            }
                        >
                            Ban User
                        </button>
                    </li>
                ))}
            </ul>

            <h3>Banned Users:</h3>
            <ul>
                {bannedUsers.map((user) => (
                    <li key={user.id}>
                        {user.email} {user.username}
                    </li>
                ))}
            </ul>
        </div>
    );
}

Enter fullscreen mode Exit fullscreen mode

We can extend this further by implementing one of the powerful capabilities of Server Components, which is Streaming, by using React Suspense. While waiting for our data to load, we can display a loading skeleton to the user, providing them a good experience.

app/page.tsx

import { Suspense } from "react";
import UserDashboard from "@/app/user-dashboard";

export interface User {
    id: string;
    email: string;
    username: string;
}

/* Fetching data with a server component and passing the data down via props*/
/* w/ Streaming */
export default async function Page() {
    return (
        <Suspense fallback={<UserDashboardLoadingSkeleton />}>
            <UserDashboardWrapper />
        </Suspense>
    );
}

async function UserDashboardWrapper() {
    const res = await fetch("https://examplebackend.com/users");
    const users: User[] = await res.json();

    return <UserDashboard users={users} />;
}

function UserDashboardLoadingSkeleton() {
    return <div>Loading...</div>;
}

Enter fullscreen mode Exit fullscreen mode

Server Actions

If we are ever going to be mutating data, we should look towards using Server Actions. We can also fetch data with Server Actions but it is not recommended as GET operations are not its intended purpose.

In the code example below, we create a form where users can submit their feedbacks. It is a basic example of how we would typically use Server Actions for form submission.

app/feedback/actions.ts

"use server";

export async function createFeedback(formData: FormData) {
    const username = formData.get("username");
    const feedback = formData.get("feedback");

    try {
        console.log({ username, feedback });
        // Database logic here...
    } catch (error) {
        // Error handling code here...
        console.error(error);
    }
}

Enter fullscreen mode Exit fullscreen mode

app/feedback/page.tsx

"use client";

import { createFeedback } from "@/app/feedback/actions";

export default function FeedbackForm() {
    return (
        <form action={createFeedback}>
            <label htmlFor="username">Your Username</label>
            <input id="username" name="username" type="text" />
            <label htmlFor="feedback">Your Feedback</label>
            <textarea name="feedback" id="feedback"></textarea>
            <button type="submit">Submit</button>
        </form>
    );
}

Enter fullscreen mode Exit fullscreen mode

Keep in mind that in real-world development, always make sure to validate your forms either natively or with established external libraries like Zod. Additionally, consider using the useActionState Hook to manage form submission state and improve interactivity.

Route Handlers

Route Handlers give us the capability to make our own custom API endpoints. We can use them for fetching data, just like Server Components. They are also pretty handy for integrating with and replying to external services. We define Route Handlers in a route.js or route.ts file, depending on whether you are using JavaScript or TypeScript.

Here is a basic example of a Route Handler that returns an array of users when we make a request to it:

app/api/users/route.ts

import { User } from "@/app/page";

export async function GET() {
    const users: User[] = [
        {
            id: "2193dzd",
            email: "example.user@gmail.com",
            username: "exampleuser1",
        },
        {
            id: "asdf8zd",
            email: "example.user2@gmail.com",
            username: "exampleuser2",
        },
    ];

    return Response.json(users);
}

Enter fullscreen mode Exit fullscreen mode

We can make a call to it with fetch by including the protocol and hostname of our app or project. For example: https://localhost:3000/api/users/

Conclusion

I hope you found this technical article insightful and helpful. If I had made any mistakes, please feel free to let me know by making a comment. Thanks for reading!

About Me

I am a freshman Computer Science student at Cavite State University. I formerly liked coffee and still do like pandas!

Connect with me:
GitHub: https://github.com/waffensultan
LinkedIn: https://www.linkedin.com/in/waffensultan/

Sentry blog image

How I fixed 20 seconds of lag for every user in just 20 minutes.

Our AI agent was running 10-20 seconds slower than it should, impacting both our own developers and our early adopters. See how I used Sentry Profiling to fix it in record time.

Read more

Top comments (0)