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