Senior Software Engineer, Jakamo
youtube.com/tuomokankaanpaa
x.com/tumee
tuomokankaanpaa.com
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
React components that individually fetch data and render entirely on the server.
Data fetching on the server
Security
Caching
Bundle sizes
Better Initial Page Load and First Contentful Paint
SEO
Streaming
Next.js uses Server Components by default inside App router.
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 allow us to write interactive UI that can be rendered on the client at request time.
Interactivity - state, effects, event listeners (immediate feedback)
Browser APIs (geolocation, localStorage, etc.)
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.
Image
Name
Price
Description
Additional info
Postgresql database
"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
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
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.
"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
"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
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
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
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
Client Component
Server Component
Data fetching on the server
Security
Caching
Bundle sizes
Better Initial Page Load and First Contentful Paint
SEO
Streaming
(Wasn't evident in the examples)
Requests can be cached between users
Data fetching is done in the server
Client bundle size is smaller
Cleaner code
Less code
Everything stays inside of the component boundaries
Senior Software Engineer, Jakamo
youtube.com/tuomokankaanpaa
x.com/tumee
tuomokankaanpaa.com