Data Fetching

In this lesson, we are going to discuss how to fetch data in various parts of a Next.js application, including API routes, server actions, server components, as well as client components. We'll also introduce and compare two data fetching patterns, sequential fetching and parallel fetching.

Just in case you need a refresher on concepts such as API routes, server actions, server components, and client components, here is a brief review:

  • Routes: Next.js implements a file based routing system, the files and folders inside the app directory maps to the routes to access the webpages and API endpoints.
  • Pages: The webpage is defined by a page.jsx file.
    • Server Components: By default, the webpages are rendered on the server to provide better performance, faster loading time, and better user experience.
    • Client Components: You can opt into client side rendering using a "use client" directive, giving access to React features such as state management, context, reducers, and more. Injecting interactivity into you web app.
  • API Route Handlers: The API endpoint is defined by a route.js file.
  • Server Actions: A piece of code that can be used inside server or client component, but will be executed on the server. Most commonly used for form handling, allows you to submit the form without creating a dedicated API endpoint.

Fetching data on the server from API

The most commonly used method for data fetching is by using the fetch() API. It is the default interface for making HTTP requests using JavaScript, and also processing the responses. For example:

jsx
1export default async function Page() {
2  const res = await fetch("https://dummyjson.com/users");
3  const data = await res.json();
4  return (
5    <ul>
6      {data.users.map((user) => (
7        <li key={user.id}>
8          {user.firstName} {user.lastName}
9        </li>
10      ))}
11    </ul>
12  );
13}

The fetch() API allows you to set extra options, including:

  • The method option allows you to specify the HTTP method used to send the request, such as GET, POST, PUT, or DELETE.
  • The body option allows you to include the request payload, often in formats like JSON, which the API endpoint can process to handle the request.
  • The headers option allows you to define additional metadata for the request, such as Content-Type to specify the media type of the request body, or Authorization to include authentication tokens.
javascript
1const res = await fetch("https://dummyjson.com/users", {
2  method: "POST",
3  body: JSON.stringify({
4    firstName: "John",
5    lastName: "Doe",
6    email: "john@thedevspace.io",
7  }),
8  headers: {
9    "Content-Type": "application/json",
10  },
11});

Fetching data on the server from database

Aside from fetching data from a remote API endpoint, another common use case is fetching data from a database.

In the Next.js community, Prisma.js is one of the most popular ORMs used for database manipulation. It simplifies database operations by providing a easy-to-use API for querying, creating, updating, and deleting data.

To start using Prisma, set up the project as we've discussed in the previous lesson, and then create a db.js file under src/libs, with the following code:

src/libs/db.js

javascript
1import { PrismaClient } from "@prisma/client";
2
3const prismaClientSingleton = () => {
4  return new PrismaClient();
5};
6
7const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();
8
9export default prisma;
10
11if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma;

This file initializes a new instance of PrismaClient(), and exported as prisma.

The prisma can then be imported into any server components, API routes, or server actions, and then used to manipulate the database like this:

javascript
1const user = await prisma.user.create({
2  data: {
3    email: "amy@thedevspace.io",
4    name: "Amy Pond",
5  },
6});

Remember that you cannot access the database in client components.

Fetching data on the client from API

Both methods discussed above are examples of server-side data fetching, which is, in fact, the recommended approach in Next.js.

Whenever you need to fetch data, prioritize fetching it on the server. This approach enables server-side caching, keeps sensitive data secure, and offers better SEO and user experience.

If the fetched data is required in client components, you can pass it down as props.

jsx
1export default async function Page() {
2  const res = await fetch("https://dummyjson.com/users");
3  const data = await res.json();
4  return <ClientComponent data={data} />;
5}

However, if in some special cases, where you do need to fetch data from the client side, here is how you can do it:

page.jsx

jsx
1"use client";
2
3import { useState, useEffect } from "react";
4
5export default function Page() {
6  const [userData, setUserData] = useState(null);
7
8  useEffect(() => {
9    async function fetchUserData() {
10      const res = await fetch("https://dummyjson.com/users");
11      const data = await res.json();
12      setUserData(data);
13    }
14    fetchUserData();
15  }, []);
16
17  if (!userData) return <div>Loading...</div>;
18
19  return (
20    <ul>
21      {userData.users.map((user) => (
22        <li key={user.id}>
23          {user.firstName} {user.lastName}
24        </li>
25      ))}
26    </ul>
27  );
28}

Notice that we cannot use the fetch() API directly, because client component does not support the async/await syntax, and instead, the data fetching logic is placed inside useEffect().

javascript
1useEffect(() => {
2  async function fetchUserData() {
3    const res = await fetch("https://dummyjson.com/users");
4    const data = await res.json();
5    setUserData(data);
6  }
7  fetchUserData();
8}, []);

The fetched data will be used to update the userData state, which is then used to update the entire component.

Alternatively, you can also place the data fetching logic inside event handlers as well.

jsx
1"use client";
2
3import { useState } from "react";
4
5export default function Page() {
6  const [userData, setUserData] = useState(null);
7  const [loading, setLoading] = useState(false);
8
9  async function fetchUserData() {
10    setLoading(true);
11    try {
12      const res = await fetch("https://dummyjson.com/users");
13      const data = await res.json();
14      setUserData(data);
15    } catch (error) {
16      console.error("Error fetching user data:", error);
17    } finally {
18      setLoading(false);
19    }
20  }
21
22  return (
23    <>
24      <button onClick={fetchUserData} disabled={loading}>
25        {loading ? "Loading..." : "Fetch Users"}
26      </button>
27      {userData && (
28        <ul>
29          {userData.users.map((user) => (
30            <li key={user.id}>
31              {user.firstName} {user.lastName}
32            </li>
33          ))}
34        </ul>
35      )}
36    </>
37  );
38}

Now, what if you need to fetch data from the database?

Well, the short answer is don't. This data fetching method is NOT recommended. But, in some rare cases, if you do need to fetch data from the database inside client components. You can design an API route that fetches the data, and call this API endpoint in your client component:

page.jsx

jsx
1"use client";
2
3import { useState, useEffect } from "react";
4
5export default function Page() {
6  const [userData, setUserData] = useState(null);
7
8  useEffect(() => {
9    async function fetchUserData() {
10      const res = await fetch("/api/users");
11      const data = await res.json();
12      setUserData(data);
13    }
14    fetchUserData();
15  }, []);
16
17  if (!userData) return <div>Loading...</div>;
18
19  return (
20    <ul>
21      {userData.users.map((user) => (
22        <li key={user.id}>
23          {user.firstName} {user.lastName}
24        </li>
25      ))}
26    </ul>
27  );
28}

Parallel fetching vs. sequential fetching

When it comes to fetching data, there are two fetching patterns you could implement, sequential fetching or parallel fetching.

Parallel vs sequential fetching

Sequential fetching is when one fetch operation is completed before the next one begins.

This fetching pattern is commonly used when the subsequent fetches rely on the response of the first fetch. For example:

jsx
1export default async function Page() {
2  const userResponse = await fetch("https://dummyjson.com/users");
3  const userData = await userResponse.json();
4
5  const firstUserId = userData.users[0]?.id;
6
7  const postResponse = await fetch(
8    `https://dummyjson.com/user/${firstUserId}/posts`
9  );
10  const postData = await postResponse.json();
11
12  return (
13    <div>
14      <h1>Users</h1>
15      <ul>
16        {userData.users.map((user) => (
17          <li key={user.id}>
18            {user.firstName} {user.lastName}
19          </li>
20        ))}
21      </ul>
22
23      <h2>Posts by First User</h2>
24      <ul>
25        {postData.posts.map((post) => (
26          <li key={post.id}>{post.title}</li>
27        ))}
28      </ul>
29    </div>
30  );
31}

In this case, we first retrieved a list of users, and then we access the id of the first user, and use it to retrieve the posts that belong to this user.

But if the requests do not rely on the previous requests, you should consider parallel fetching instead. This allows multiple fetch operations to be started simultaneously and resolved independently.

jsx
1async function getUsersData() {
2  const res = await fetch("https://dummyjson.com/users");
3  return res.json();
4}
5
6async function getPostsData() {
7  const res = await fetch("https://dummyjson.com/posts");
8  return res.json();
9}
10
11export default async function Page() {
12  const [usersData, postsData] = await Promise.all([
13    getUsersData(),
14    getPostsData(),
15  ]);
16
17  return (
18    <div>
19      <h1>Users</h1>
20      <ul>
21        {usersData.users.map((user) => (
22          <li key={user.id}>
23            {user.firstName} {user.lastName}
24          </li>
25        ))}
26      </ul>
27
28      <h2>Posts</h2>
29      <ul>
30        {postsData.posts.map((post) => (
31          <li key={post.id}>{post.title}</li>
32        ))}
33      </ul>
34    </div>
35  );
36}

To fetching data in parallel, you can define the data fetching logic outside of the component that need to use it, and then initialize the logic in parallel using Promise.all().

And lastly, if you have nested components, where each component fetches different data, the fetch operation will happen sequentially. The layout and pages, on the other hand, are rendered in parallel, which means the data fetching will also happen in parallel.