Data Fetching and Mutations in Next.js

An introduction to best practices using server components and server actions in Next.js

Written by Vishrut Agrawal

July 14, 2024

Prerequisites

This article assumes you are comfortable with React and are familiar with Next.js and the basics of the App Router.

What's different about Next.js?

It's first important to understand what Next.js offers compared to other frameworks. Next.js offers many cutting edge features that are brand new and confusing to many developers, including me. These features include:

  • Server Components - data fetching
  • Server Actions - data mutations

Server Components were introduced in Next.js version 13, and Server Actions were introduced in version 14. Based on the version of Next.js you are using, certain features may not be available.

Data Fetching

What are Server Components?

Server Components are React components that are exclusively run on the server. Server Components bring many benefits to the table, including:

  • Data Fetching and Performance - Data fetching can be done in a server component, which makes data fetching more efficient and reduces the number of operations that the client needs to perform
  • Security - All sensitive data is exposed to the server, which makes it harder for an attacker to exploit your application
  • Developer Experience - Server Components make data fetching and rendering much easier to manage for a developer, which will be seen later in the article
  • SEO - Since the server sends strictly HTML to the client, search engines can index pages faster and more accurately

By default, all Next.js components are server components. To make a client component, you can add a 'Use Client' directive to the top of the file. This will tell Next.js to render the page or component on the client. This is useful for pages that will need to use any client-side functionality, such as:

  • React hooks such as useState or useEffect
  • Calls to window or document objects, or any other browser APIs
  • Event handlers such as onClick or onChange

In general, you should only use client components when they are needed, as they easily interweave with server components on a page.

Data fetching using Server Components

With Server Components, data fetching has never been easier. Here's a quick example of how to fetch data in a server component

// page.tsx
import { fetchData } from '@/data/example'
 
export default async function Page() {
  const data = await fetchData()
 
  return (
    // use the data in the page...
  )
}

All you have to do is mark the function as 'async' and then make any database calls. And because of Next.js's built in caching, the page will be saved until the cache is invalidated, reducing duplicate data fetches.

fetchData is a direct database call, and because it is a server component, no database secrets are exposed to the client.

Another example of a data fetching function is shown below, and it is made of snippets from the SweetBeasts Source Code

// account/orders/page.tsx
import { Suspense } from 'react'
import OrderPageSkeleton from '@/...'
import OrderPageContent from '@/...'
 
export default function OrderPage() {
  return (
    <main>
      <h1>Your Orders</h1>
      <Suspense fallback={<OrderPageSkeleton />}>
        <OrderPageContent />
      </Suspense>
    </main>
  )
}
 
// components/general/orders/order-page-content.tsx
import { getAllOrdersByUser } from '@/data/orders'
import { currentUser } from '@/lub/auth'
 
export default async function OrderPageContent() {
  const user = await currentUser()
  const orders = await getAllOrdersByUser(user.id)
 
  return (
    // render the orders here...
  )
}

With this example, we are using a React Suspense boundary to show a skeleton (OrderPageSkeleton) while the data is being fetched. This improves the user experience as the page instantly loads with a loading state to indicate that data is being fetched.

These examples are just a small taste of what you can do with server components. When you need to fetch data in a client component, a simple approach is to fetch the data in a server component and pass it as a prop to a client component. An example of this is shown below:

// server-component.tsx
import { fetchData } from '@/data/example'
 
export default async function Page() {
  const data = await fetchData()
 
  return (
    <ClientComponent initialData={data} />
  )
}
 
// client-component.tsx
'use client'
 
import { useState } from 'react'
 
export default function ClientComponent( {initialData}: { initialData: any }) {
  const [initialData, setInitialData] = useState(initialData)
 
  return (
    // use the data in the client component...
  )
}

This approach is useful for when you need to fetch data in a client component, as it is very straightforward with only a few lines of code.

Overall, server components are a great way to fetch data in a Next.js application, and it is worth using them in future projects as they are a great way to improve the developer and user experience.

Data Mutations

Server Actions

Server Actions are much more straightforward than server components. Server Actions are POST endpoints generated at build time. Compared to a normal API route, they are much easier to use and heavily increase the developer experience by adding end to end type safety. Server Actions can be defined with a 'Use Server' directive at the top of a file, which will create a server action for any function exported from the file. You can also place the 'Use Server' directive at the top of a function to create a server action for that function.

Since all server actions are converted to POST endpoints, they will be accessibly publicly, so be sure to secure them with any authorization as you would for any other API route. For more information, watch this video

Using Server Actions

Client Components

  • In a client component, you can import the server action as you would any other function and call it directly. It's that simple.

Server Components

  • In a server component, you can import the server action as you would, or you can define a server action in the same file and call it via a form's action prop.
// server-component.tsx
 
export default function Form() {
  const handleSubmit = async (form: FormData) => {
    'use server'
    // handle the form submission
  }
 
  return (
    <form action={handleSubmit}>
    {/* form fields */}
    </form>
  )
}
 

The way that I defined a server action above is only possible in server components. Using a server action in a client component will require an export from a file.

Mutations with Server Actions

Server Actions are a perfect way to create mutations in Next.js applications. Here's an example of a simple mutation using a server action in a TODO list app:

// page.tsx
import { db } from '@/data/prisma' // can use any ORM or direct query
import { addTodo } from '@/actions/add-todo'
 
export default async function TodoPage() {
  const todos = await db.todo.findMany()
 
  return (
    <main>
      <form action={addTodoServerAction}>
        <input type="text" />
        <button type="submit">Add Todo</button>
      </form>
      <ul>
        {todos.map((todo) => (
          <li>{todo.message}</div>
        ))}
      </ul>
    </main>
  )
}
 
// add-todo.ts
'use server'
 
import { db } from '@/data/prisma'
 
export default async function addTodo(message: string) {
  await db.todo.create({ data: message })
  revalidatePath('/todos')
}

There's a lot going on here, so let's break it down step by step, starting with the page:

  1. We have an input field and a button to add a new todo, as well as a list of existing todos. (very simple app so we're not able to mark them as completed)
  2. When the user submits the form, the addTodo server action is called, which creates a new todo in the database, and then calls the revalidatePath function
  3. revalidatePath tells Next.js to revalidate the cache, and Next.js will automatically update the page with the new todo without having to reload the page

All in all, serve actions are a great way to mutate data in your application, and they can be used in both client and server components. The DX improvement that they provide is huge, and they're a great way to add end to end type safety to your application.

Conclusion and more resources

When I started building Next.js applications, I was pretty lost on how these new features worked and what best practices were, but after countless hours of digging into the documentation and experimenting with different approaches, I've come to the conclusion that server components and server actions are the way to go. Nothing in this article is a secret or earth shattering information, but I believe that it compiles a few things that me and many developers struggled to understand at first.

If you want to learn more about server components and server actions, I will link you to the pages in the Next.js docs.

I hope you find this article helpful and that it helps you to better understand how to use server components and server actions in your Next.js applications, for any feedback, questions or comments, please reach out. My contact info is just a few pixels down the page.

Thanks for reading!

©2024 Vishrut Agrawal