Tuomo Kankaanpää

Senior Software Engineer, Jakamo

youtube.com/tuomokankaanpaa

x.com/tumee

Data fetching & mutations in Next.js 14

tuomokankaanpaa.com

React Server Components (RSC)

App router uses Server Components by default

app router (beta)

Next.js 13

Oct 2022

app router (stable)

Next.js 14

Oct 2023

New ways to fetch and post data

Server Components (RSC)

Client Components (RCC)

Data fetching examples with RCC & RSC

Server Components

React components that individually fetch data and render entirely on the server.

Benefits

Data fetching on the server

Security

Caching

Bundle sizes

Better Initial Page Load and First Contentful Paint

SEO

Streaming

How

Next.js uses Server Components by default inside App router.

When

Fetch data

Access backend resources (directly)

Keep sensitive information on the server

Reduce client-side Javascript

When you don't need client specific features

Client Components

Client Components allow us to write interactive UI that can be rendered on the client at request time.

When & Why

Interactivity - state, effects, event listeners (immediate feedback)

Browser APIs (geolocation, localStorage, etc.)

How

Client components are opt-in in Next.js (app router).

Add "use client" declaration at the top of a file.

All other modules imported into client component, including child components, are considered part of the client bundle.

Use cases

Examples

Image

Name

Price

Description

Additional info

Postgresql database

Client Component implementation

"use client";
import { useEffect, useState } from "react";

export default function Product(props) {
  const [data, setData] = useState(null);
  const [isLoading, setLoading] = useState(true);
  useEffect(() => {
    fetch(`/api/product/${props.id}`)
      .then((res) => res.json())
      .then((data) => {
        setData(data);
        setLoading(false);
      });
  }, []);

  if (isLoading) return <p>Loading...</p>;
  if (!data) return <p>No product data</p>;
  
  return (
    <div>...</div>
  );
}
app/product/Product.js

Route Handlers

Creates API endpoint

Defined in route.js file inside the app directory

GET, POST, PUT, PATCH, DELETE, HEAD and OPTIONS

Equivalent of API Routes inside the pages directory

Executes on the server & returns the data to the client

import { sql } from "@vercel/postgres";

export async function GET(request, { params }) {
  const product = await sql`select * from products where id = ${params.id}`;
  
  return Response.json(product.rows[0]);
}
app/api/product/[id]/route.js
{
  "name": "Red T-Shirt",
  "price": "47 EUR",
  "id": 1,
  "description": "Experience the perfect blend of comfort and style with our premium cotton t-shirt, featuring a classic fit and a versatile design ideal for any occasion.",
  "additional_info": "Fabric: 100% Cotton. Care: Machine wash cold, tumble dry low.",
  "image": "/red-t-shirt.png"
}
GET /api/product/1
import Product from "./Product";

export default function ProductPage() {
  return (
    <div>
      <h1>awesome store</h1>
      <Product id={1} />
    </div>
  );
}
app/product/page.js

Server Component implementation

import { sql } from "@vercel/postgres";

const getData = async (id) => {
  const product = await sql`select * from products where id = ${id}`;

  return product.rows[0];
};

export default async function Product(props) {
  const data = await getData(props.id);

  if (!data) return <p>No product data</p>;

  return (
    <div>...</div>
  );
}
app/product/Product.js

No need for a route handler!

import Product from "./Product";

export default function ProductPage() {
  return (
    <div>
      <h1>awesome store</h1>
      <Product id={1} />
    </div>
  );
}
app/product/page.js

No loading indicator

import { Suspense } from "react";
import Product from "./Product";

export default function ProductPage() {
  return (
    <div>
      <h1>awesome store</h1>
      <Suspense fallback={<div>Loading...</div>}>
      	<Product id={1} />
      </Suspense>
    </div>
  );
}
app/product/page.js

Feature for managing async operations

Lets you display a fallback until its children have finished loading.

Client Component implementation

Reviews list

"use client";
import { useEffect, useState } from "react";

export default function Reviews(props) {
  const [data, setData] = useState(null);
  const [isLoading, setLoading] = useState(true);

  useEffect(() => {
    fetch(`/api/product/${props.id}/reviews`)
      .then((res) => res.json())
      .then((data) => {
        setData(data);
        setLoading(false);
      });
  }, []);

  if (isLoading) return <div>Loading reviews...</div>;
  
  return (
    <section>
      {data.map((r) => {
        return (
          <div key={r.id}>
            <p>{r.name}</p>
            <p>{r.text}</p>
          </div>
        );
      })}
    </section>
  );
}
app/product/Reviews.js
import { sql } from "@vercel/postgres";

export async function GET(request, { params }) {
  const reviews =
    await sql`select * from reviews where ref_product = ${params.id}`;

  return Response.json(reviews.rows);
}
app/api/product/[id]/reviews/route.js
[
  {
  	"id": 59,
 	"ref_product": "1",
  	"name": "Tuomo Kankaanpää",
  	"text": "This shirt was super comfortable. I really liked the red color too! I would recommend."
  }
]
GET /api/product/1/reviews
import Product from "./Product";
import Reviews from "./Reviews";

export default function ProductPage() {
  return (
    <div>
      <h1>awesome store</h1>
      <Product id={1} />
      <Reviews id={1} />
    </div>
  );
}
app/api/product/page.js

Review form

"use client";
import { useEffect, useState } from "react";

export default function Reviews(props) {
  // ...
  const [name, setName] = useState("");
  const [text, setText] = useState("");

  // ...  
  const submitForm = () => {
    const payload = { name, text };

    fetch(`/api/product/${props.id}/reviews`, {
      method: "POST",
      body: JSON.stringify(payload),
    })
    .then((res) => res.json())
    .then((responseData) => setData(responseData));
  };
  
  // ...

  return (
    <section>
	  // ...
      <p>Post a review</p>
      <form>
        <input type="text" value={name} onChange={(e) => setName(e.target.value)} />
    	<textarea value={text} onChange={(e) => setText(e.target.value)}></textarea>
    	<button onClick={submitForm}>Save</button>
   	  </form>
    </section>
  );
}
app/product/Reviews.js
"use client";
import { useEffect, useState } from "react";

export default function Reviews(props) {
  const [data, setData] = useState(null);
  const [isLoading, setLoading] = useState(true);
  const [name, setName] = useState("");
  const [text, setText] = useState("");

  useEffect(() => {
    fetch(`/api/product/${props.id}/reviews`)
      .then((res) => res.json())
      .then((data) => {
        setData(data);
        setLoading(false);
      });
  }, []);
  
  const submitForm = () => {
    const payload = { name, text };

    fetch(`/api/product/${props.id}/reviews`, {
      method: "POST",
      body: JSON.stringify(payload),
    })
    .then((res) => res.json())
    .then((responseData) => setData(responseData));
  };
  
  if (isLoading) return <div>Loading reviews...</div>;

  return (
    <section>
	  {data.map((r) => {
        return (
          <div key={r.id}>
            <p>{r.name}</p>
            <p>{r.text}</p>
          </div>
        );
      })}
      <p>Post a review</p>
      <form>
        <input type="text" value={name} onChange={(e) => setName(e.target.value)} />
    	<textarea value={text} onChange={(e) => setText(e.target.value)}></textarea>
    	<button onClick={submitForm}>Save</button>
   	  </form>
    </section>
  );
}
import { sql } from "@vercel/postgres";

export async function GET(request, { params }) {
  const reviews =
    await sql`select * from reviews where ref_product = ${params.id}`;

  return Response.json(reviews.rows);
}

export async function POST(req, { params }) {
  const { name, text } = await req.json();
  
  await sql`insert into reviews (name, text, ref_product) values (${name}, ${text}, ${params.id})`;

  const reviews =
    await sql`select * from reviews where ref_product = ${params.id}`;

  return Response.json(reviews.rows);
}
app/api/product/[id]/reviews/route.js

Server Component implementation

Reviews list

import { sql } from "@vercel/postgres";

const getReviews = async (id) => {
  const reviews =
    await sql`select * from reviews where ref_product = ${props.id}`;
  return reviews.rows;
};

export default async function Reviews(props) {
  const data = await getReviews(props.id);

  return (
    <section>
      {data.map(({ name, text }) => {
        return (
          <div>
            <p>{name}</p>
            <p>{text}</p>
          </div>
        );
      })}
    </section>
  );
}
app/product/Reviews.js

No need for a Route Handler!

import Product from "./Product";
import Reviews from "./Reviews";

export default function ProductPage() {
  return (
    <div>
      <h1>awesome store</h1>

      <Product id={1} />
      <Reviews id={1} />
    </div>
  );
}
app/product/page.js
import { Suspense } from "react";
import Product from "./Product";
import Reviews from "./Reviews";

export default function ProductPage() {
  return (
    <div>
      <h1>awesome store</h1>

      <Suspense fallback={<div>Loading product...</div>}>
        <Product id={1} />
      </Suspense>

      <Suspense fallback={<div>Loading reviews...</div>}>
        <Reviews id={1} />
      </Suspense>
    </div>
  );
}
app/product/page.js

Review form

import { sql } from "@vercel/postgres";
import { revalidatePath } from "next/cache";

// ...

export default async function Reviews(props) {
  // ...
  const submitForm = async (formData) => {
    "use server";
    const name = formData.get("name");
    const text = formData.get("text");
    const id = formData.get("id");
    
    await sql`insert into reviews (name, text, ref_product) values (${name}, ${text}, ${id})`;

    revalidatePath("/product");
  };

  return (
    <section>
	  // ...
	  <p>Post a review</p>
      <form action={submitForm}>
        <input type="text" name="name" />
        <textarea name="text"></textarea>
        <input type="hidden" value={props.id} name="id" />
        <button type="submit">Save</button>
      </form>
    </section>
  );
}
app/product/Reviews.js
revalidatePath

Purges cached data on-demand for a specific path

Server component will update its data automatically

import { sql } from "@vercel/postgres";
import { revalidatePath } from "next/cache";

// ...

export default async function Reviews(props) {
  // ...
  const submitForm = async (formData) => {
    "use server";
    const name = formData.get("name");
    const text = formData.get("text");
    const id = formData.get("id");
    
    await sql`insert into reviews (name, text, ref_product) values (${name}, ${text}, ${id})`;

    revalidatePath("/product");
  };

  return (
    <section>
	  // ...
	  <p>Post a review</p>
      <form action={submitForm}>
        <input type="text" name="name" />
        <textarea name="text"></textarea>
        <input type="hidden" value={props.id} name="id" />
        <button type="submit">Save</button>
      </form>
    </section>
  );
}
app/product/Reviews.js
import { sql } from "@vercel/postgres";
import { revalidatePath } from "next/cache";

const getReviews = async (id) => {
  const reviews =
    await sql`select * from reviews where ref_product = ${props.id}`;
  return reviews.rows;
};

export default async function Reviews(props) {
  const data = await getReviews(props.id);
  
  const submitForm = async (formData) => {
    "use server";
    const name = formData.get("name");
    const text = formData.get("text");
    const id = formData.get("id");
    
    await sql`insert into reviews (name, text, ref_product) values (${name}, ${text}, ${id})`;

    revalidatePath("/product");
  };

  return (
    <section>
	  {data.map(({ name, text }) => {
        return (
          <div>
            <p>{name}</p>
            <p>{text}</p>
          </div>
        );
      })}
	  <p>Post a review</p>
      <form action={submitForm}>
        <input type="text" name="name" />
        <textarea name="text"></textarea>
        <input type="hidden" value={props.id} name="id" />
        <button type="submit">Save</button>
      </form>
    </section>
  );
}
app/product/Reviews.js

What is the difference then?

Client Component

Server Component

Data fetching on the server

Security

Caching

Bundle sizes

Better Initial Page Load and First Contentful Paint

SEO

Streaming

Benefits of using Server Components

(Wasn't evident in the examples)

Server Components are faster

Requests can be cached between users

Data fetching is done in the server

Client bundle size is smaller

Benefits of using Server Components

Cleaner code

Less code

Everything stays inside of the component boundaries

We still need Client Components too

Tuomo Kankaanpää

Senior Software Engineer, Jakamo

youtube.com/tuomokankaanpaa

x.com/tumee

Thank you!

tuomokankaanpaa.com

Next.js data fetching & mutations

By tume

Next.js data fetching & mutations

Data fetching is a core part of any application. Since the introduction of the app router, we now have multiple ways we can fetch and mutate data in our Next.js application. In this talk we will take a look on different ways to handle data fetching & mutations in Next.js 14 application.

  • 32