NextJS 13 / Nextjs 14
Router
Static Routes
https://example.com/hello
Directory Structure:
app/hello/page.tsx
Moving to another page
- Next
Link
<Link href={'/about'}> Hello</Link>
useRouter
Programatic change of route
'use client';
import { useRouter } from 'next/router';
export default function Home() {
const router = useRouter();
const someEvent = () => {
router.push('/someOtherPage');
router.back();
router.reload();
}
return <div></div>
}
[Dynamic]
Routes
https://example.com/hello/:id
Directory Structure with [param_name]
:
app/hello/[id]/page.tsx
Access Route Parameters
- In server components it is available in the
params
prop
interface Props {
params: {
id: string;
}
}
export default async function Home({ params: { id }}) {
const book = await fetch(`https://.../api/book/${id}`);
return {
<h1>{book}</h1>
}
}
- In client components it is available using
useParams
:
'use client';
import { useParams } from 'next/navigation';
export default function Home() {
const { id } = useParams();
return {
<h1>{id}</h1>
}
}
IMPORTANT: A route can have as many params as you need!
https://example.com/hello/:id/:name/:genre
Directory Structure with [param_one]/[param_two]
etc.:
app/hello/[id]/[name]/[genre]/page.tsx
interface Props {
params: {
id: string;
name: string;
genre: string;
}
}
export default async function Home({ params: { id, name, genre }}) {
// ...
}
Similarly for client usage.
Dynamic [...CatchAll]
Route
https://example.com/hello/:id/:id/:id
Directory Structure with [...param_name]
:
app/hello/[...id]/page.tsx
Route (group)
Directory Structure with (group_name)
:
app/(marketing)/about/page.tsx
https://example.com/about
app/(marketing)/blog/page.tsx
https://example.com/blog
app/(shop)/account/page.tsx
https://example.com/account
@Parallel
Route
Creates a named slot that can be accessed in the parent layout
(..)Intercepting
Route
This pattern renders the default page for the server side rendering, but then uses an entirely different page for the client side rendering.
Directory structure to show modal if on feed:
[feed]/(..)photo/[id]/page.tsx
[feed]/layout.tsx
photo/[id]/page.tsx
photo/[id]/layout.tsx
Route handlers
If you want to use an endpoint as API, just create the directory structure and add a route.tsx
instad of a page.tsx
// GET POST PUT PATCH DELETE HEAD OPTIONS
export async function GET() {
return new Response('Hello!');
}
export async function POST(request: Request) {
const data = await request.json();
// treat
return new Response('Updated');
}
NOTE: They are always server side.
Request / Response vs NextRequest / NextResponse
// by default run in nodejs runtim
// export const runtime = 'nodejs' by default
// can be changed to different things
export const runtime = 'edge';
import { NextRequest, NextResponse } from 'next/server';
export async function PATCH(request: NextRequest) {
const url = request.nextUrl;
return NextResponse.json({ message: 'I want to send JSON!' });
}
Layouts
Layouts wrap pages.
IMPORTANT: layout UI and state persist accross page changes. So if that's not what you want, you can reinitialize the layout component for every navigation using template.tsx
instead of layout.tsx
.
Root Layout
By default the application has a RootLayout
that defines the entire UI
for the entire applicaiton.
interface ComponentProps {
children: React.ReactNode;
}
export default function RootLayout({ children }: ComponentProps) {
return (
<html lang="en" data-mode="light">
<body>
<main className="relative">
{children}
</main>
</body>
</html>
);
}
Nested Layout
You can nest layouts, by for example having a route with the main navigation from the RootLayout, and an aditional sidebar for a dashboard.
Directory structure:
app/layout.tsx
app/dashbaord/page.tsx
app/dashbaord/layout.tsx
app/dashbaord/navbar.tsx
interface ComponentProps {
children: React.ReactNode;
}
export default function Layout({ children }: ComponentProps) {
return (
<div id="dashbaord">
<NavMenu /> // does not need to rerender
{children}
</div>
);
}
Template Layouts
template.tsx
instead of layout.tsx
forces a rerender of the layout for every navigation.
Rendering and SEO
The mental model is very simple now.
By default you want to make every page.tsx
a server component.
What is a bit confusing is that in that page, you might want to use client side features in a server component like useEffect
and that won't compile for a server component.
useEffect(() => { // client-side react : FAIL on server side components
console.log('Component mounted');
}, []);
return (
<div id="dashbaord">
{children}
</div>
);
The solution is to move the interactive code into its own client component file (with "use client"
).
Move to Client Component
You move the interactive-code from a **server-side ** component into it's own client-component.
Server modules:
// page.tsx
import { ServerComponent } from '...';
export default function Page() {
return <ServerComponent />;
}
// ServerComponent.tsx
import { ClientComponent } from '...';
export default function ServerComponent() {
return <ClientComponent />;
}
Client modules:
// ClientComponent.tsx
"use client"
import { MyButton } from '...';
export default function ClientComponent() {
return <MyButton onClick="..." />;
}
// MyButton.tsx
"use client"
export default function MyButton({ clickHandler }) {
return <button onClick={clickHandler} />;
}
You want to move the client components to the leaves of the tree. This allows your top level pages to be server rendered and SEO optimized the most parts.
page.tsx
settings
Caching mechanism
-
Dynamic Data Fetching: data fetched on each request
export const dynamic = 'force-dynamic' // similar to SSR's getServerSideProps in prev versions export default function Page() { return ( <main> </main> ); }
- WHEN ? Use this setup when you have data that changes very often
-
Hybrid Data Fetching: nextjs tries to cache the page to minimize load time, but still tries to figure out if data has changed to bust the cache
export const dynamic = 'auto' // by default export default function Page() { return ( <main> </main> ); }
-
Static Data Fetching: nextjs caches the page to optimize load time, never busts cache
export const dynamic = 'force-static' // cache indefinitely export default function Page() { return ( <main> </main> ); }
-
You can also revalidate a page after a certain timelapse:
export const revalidate = 6900 // seconds to cache busting (liek ISR, in old nextjs)
SEO metadata
If you export a metadata object in your page.tsx
or layout.tsx
you can optimize for SEO
export const metadata = {
title: 'Hello!',
description: "I'm so goofy"
}
// or as a function
export async function generateMetadata({ params }: any) {
return {
title: '...get from somewhere',
}
}
export default function Page() {
return (
<main>
</main>
);
}
Data fetching
In nextjs 14, every page.tsx
and layout.tsx
are server components by default, so they can access:
- env variables
- databases
- etc.
And they can do so, without the need to pass props to the component (old style) with get[ServerSide/Static]Props() // not needed anymore
.
Deduping
Components can also be async
so you can use fetch
or any other library to fetch stuff.
But nextjs overrides fetch
to track any duplicate requests, and prevents fetching the same data multiple times.
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const a = await fetch('thing');
return (
<html lang="en">
<body>
{children}
</body>
</html>
);
}
export default async function Home() {
const a = await fetch('thing'); // not fetched twice by default
const a = await fetch('thing', {
cache: 'force-cache', // cached
});
const a = await fetch('thing', {
cache: 'no-store', // highly dynamic data, never cache
});
const a = await fetch('thing', {
next: { revalidate: 234, } // refetch if seconds have passed
});
return (
<main>
</main>
);
}
Page Rendering
Steps
- Fetch data from the server (nodejs)
- Render server component into html on the server
- Send html to browser
- Render html and css as non interactive
- javascript is executed and react is loaded to hydrate the page
Streaming
Break the application in smaller chunks to load them progressively. So each chunk goes into it's own:
- Fetching data from server
- Rendering HTML on the server
- Loading code on the client
- Hydrating
This lets partial content fetching with loading state on the front end.
loading.tsx
For any page.tsx / layout.tsx
you can create a loading.tsx
component and nextjs will render the loading component.
// loading.tsx
export default function Loading() {
return <div>Loading...</div>;
}
Under the hood, this works with React.Suspense
. You can also use <Suspense>
directly into any component like so to get more granualr control:
// page.tsx
import { Suspense } from 'react';
export default async function Home() {
return (
<>
<Suspense fallback={<p>Loading posts...</p>}>
<Posts />
</Suspense>
<Suspense fallback={<p>Loading people...</p>}>
<People />
</Suspense>
<>
);
}
Suspense wraps an asynchronous component and renders the fallback as long as the promise has not resolved.
SSR (Server Side Rendering)
This is a server side renedered page: nextjs creats a cashed version (dynamic = 'auto'
), and determines when to refetch the data.
// app/blog/[slug]/page.tsx
interface Post {
title: string;
content: string;
slug: string;
}
interface Props {
params: { slug: string; };
}
export default async function BlogPostPage({ params }: Props) {
const posts: Post[] = await fetch('http://localhost:3000/api/content');
const post = posts.find(post => post.slug === params.slug)!;
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
We could alter the caching mechanism and make it dynamic, by exporting const dynamic = 'force-dynamic'
to make it always fetch the latest data on every request.
Static Pages
The most basic type of page that we can create is a Static Page. Which is basically just compiled html that is sent to the client.
// app/about/page.tsx
// if we want to be very explicit we would use:
export const dynamic = 'force-static';
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'About us!',
description: "Best company ever"
}
export default function AboutPage() {
return (
<main>
<h1>About</h1>
<p>we don't sell anything</p>
</main>
)
}
This would render within the layout and be compiled at compile time.
Dynamic API Route
The most basic routing primitive in nextjs is the route.ts
Let's add a api/content/route.ts
import { NextResponse } from 'next/server';
// some content to be returned
const posts = [
{
title: "hello",
content: "hey",
slug: "hello",
},
{
title: "hello 2",
content: "hey 2",
slug: "hello-2",
},
{
title: "hello 3",
content: "hey 3",
slug: "hello-3",
},
// ...
]
export default async function GET() {
return NextResponse.json(posts);
}
// visiting https://example.com/content would return the json blog posts
Remote Images
To use remote images with <Next.Image src="https://server-different-than-next-host.com/image.png" />
, we need to adapt the next.config.js
:
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'avatars.githubusercontent.com',
port: '',
pathname: '/u/**',
},
]
}
}
module.exports = nextConfig;
Authentication
Protected Routes
Whenever a user is not logged in, you might want to prevent then from accessing certain routes. You have different options:
-
Return some message stating
"You are not allowed"
import { getServerSession } from 'next-auth'; export default async function Home() { const session = await getServerSession(); if (!session) { return <p>Please login</p>; } return { <h1>{session.user?.email?}</h1> } }
-
Redirect to a different route (like sign in)
import { getServerSession } from 'next-auth'; import { redirect } from 'next/navigation'; export default async function Home() { const session = await getServerSession(); if (!session) { return redirect('/api/auth/signin'); // magic rout from next-auth } return { <h1>{session.}</h1> } }
Data Fetching
Server Component Fetching
You don't need to call the api, you can directly access the data from the server within the component.
// app/user/[id]/page.tsx
import FollowButton from '@/components/FollowButton/FollowButton';
import { prisma } from '@/lib/prisma';
import { Metadata } from 'next';
interface Props {
params: {
id: string;
};
}
// can use the params from the route
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const user = await prisma.user.findUnique({ where: { id: params.id } });
return { title: `User profile of ${user?.name}` };
}
export default async function UserProfile({ params }: Props) {
const user = await prisma.user.findUnique({ where: { id: params.id } });
const { name, bio, image, id } = user ?? {};
return (
<div>
<h1>{name}</h1>
<img
width={300}
src={image ?? '/mememan.webp'}
alt={`${name}'s profile`}
/>
<h3>Bio</h3>
<p>{bio}</p>
</div>
);
}
error.tsx
Error components let you display something wheever there is an error. They receive the error. IMPORTANT: must be a client component.
// app/user/[id]/error.tsx
'use client'; // Error components must be Client components
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
Data Mutations
We want to create a user dashbord to allow the user to change his data.
app/dashboard/page.tsx
import { getServerSession } from 'next-auth';
export default async function Dashboard() {
const session = await getServerSession();
if (!session) {
return redirect('/api/auth/signin'); // magic rout from next-auth
}
const currentUserEmail = session?.user?.email!;
const user = await prisma.user.findUnique({
where: {
email: currentUserEmail,
}
});
return {
<h1>Dashboard</h1>
<ProfileForm user={user} /> // defined below
}
}
Form Submission
Let's create a form (client side) that will let them modify their data.
On submit will make a request to /api/user
, that api endpoint does not exist yet, but we'll create it later.
'use client';
export function ProfileForm({ user }: any) {
const updateUser = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); // make sure not to refresh the page
const formData = new FormData(e.currentTarget);
const body = {
name: formData.get('name'),
bio: formData.get('bio'),
age: formData.get('age'),
image: formData.get('image'),
};
const res = await fetch('/api/user', {
method: 'PUT',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
},
});
await res.json();
};
return (
<div>
<h2>Edit Your Profile</h2>
<form onSubmit={updateUser}>
<label htmlFor="name">Name</label>
<input type="text" name="name" defaultValue={user?.name ?? ''} />
<label htmlFor="bio">Bio</label>
<textarea
name="bio"
cols={30}
rows={10}
defaultValue={user?.bio ?? ''}
></textarea>
<label htmlFor="age">Age</label>
<input type="text" name="age" defaultValue={user?.age ?? 0} />
<label htmlFor="image">Profile Image URL</label>
<input type="text" name="image" defaultValue={user?.image ?? ''} />
<button type="submit">Save</button>
</form>
</div>
);
}
API Mutation
Let's create tha API endpoint to modify the user data.
IMPORTANT: determine the USER FROM THE SERVER SIDE SESSION. Modify only the authenticated user, and not from provided form.
// app/api/user/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { prisma } from '@/lib/prisma';
export async function POST(req: Request) {
const session = await getServerSession();
const currentUserEmail = session?.user?.email!;
const { targetUserId } = await req.json();
const currentUserId = await prisma.user
.findUnique({ where: { email: currentUserEmail } })
.then((user) => user?.id!);
const record = await prisma.follows.create({
data: {
followerId: currentUserId,
followingId: targetUserId,
},
});
return NextResponse.json(record);
}
export async function DELETE(req: NextRequest) {
const session = await getServerSession();
const currentUserEmail = session?.user?.email!;
const targetUserId = req.nextUrl.searchParams.get('targetUserId');
const currentUserId = await prisma.user
.findUnique({ where: { email: currentUserEmail } })
.then((user) => user?.id!);
const record = await prisma.follows.delete({
where: {
followerId_followingId: {
followerId: currentUserId,
followingId: targetUserId!,
},
},
});
return NextResponse.json(record);
}
Server Pre-Hydration
Follow Button
We want to create a follow button. Whenever the user, visits another user's profile, then see a "Follow/Unfollow" button depending on their current relationship.
To determine whether the user is already following them we will first create a Server component that returns a pre-hydrated Client component
// app/components/FollowButton/FollowButton.tsx
import { getServerSession } from 'next-auth';
import FollowClient from './FollowClient';
import { prisma } from '@/lib/prisma';
interface Props {
targetUserId: string;
}
export default async function FollowButton({ targetUserId }: Props) {
const session = await getServerSession();
const currentUserId = await prisma.user
.findUnique({ where: { email: session?.user?.email! } })
.then((user) => user?.id!);
const isFollowing = await prisma.follows.findFirst({
where: { followerId: currentUserId, followingId: targetUserId },
});
return (
<FollowClient targetUserId={targetUserId} isFollowing={!!isFollowing} />
);
}
And then we create the Client component that receives the props from the server component.
// components/FollowButton/FollowClient.tsx
"use client"
import { useRouter } from 'next/navigation';
import { useState, useTransition } from 'react';
interface Props {
targetUserId: string;
isFollowing: boolean;
}
export default function FollowClient({ targetUserId, isFollowing }: Props) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const [isFetching, setIsFetching] = useState(false);
const isMutating = isFetching || isPending;
const follow = async () => {
setIsFetching(true);
const res = await fetch('/api/follow', {
method: 'POST',
body: JSON.stringify({ targetUserId }),
headers: {
'Content-Type': 'application/json'
}
});
setIsFetching(false);
console.log(res)
startTransition(() => {
// Refresh the current route:
// - Makes a new request to the server for the route
// - Re-fetches data requests and re-renders Server Components
// - Sends the updated React Server Component payload to the client
// - The client merges the payload without losing unaffected
// client-side React state or browser state
router.refresh();
});
}
const unfollow = async () => {
setIsFetching(true);
const res = await fetch(`/api/follow?targetUserId=${targetUserId}`, {
method: 'DELETE',
});
setIsFetching(false);
startTransition(() => router.refresh() );
}
if (isFollowing) {
return (
<button onClick={unfollow}>
{!isMutating ? 'Unfollow' : '...'}
</button>
)
} else {
return (
<button onClick={follow}>
{!isMutating ? 'Follow' : '...'}
</button>
)
}
}
IMPORTANT: for demonstration purposes, because this should actually all be client component to prevent full route rerendering after an update.
Server Actions
The are useful to update user data.
Imagine a Dynamic Route app/user/[id]
(displays user profile)
and another Dynamic Route app/user/[id]/edit
(allows user to edit profile)
Before Server Actions, you would
- Create an API route
app/api/user
that uses a POST / PUT request update the data - Create a client component
"use client"
that usesfetch
to make a request to that endpoint - To update the changes in the page, you would import
useTransition
from react - Finally we tell the client side router to refretch the page:
router.refresh()
to reflect changes
Now with Server Actions you:
NOTE: First enable server actions:
const nextCOnfig = {
experimental: {
serverActions: true,
},
};
module.exports = nextConfig;
Then we create the server side edit page that returns a special form
.
- It is pre-hydrated with the user data
- The form has a special property
action={upDog}
instead of the usual client sideonSubmit
upDog
is special because it contains the"use server"
directive. Nextjs will turn it into a server-side endpoint- When button is clicked a POST request is made to
dogs/[id]/edit
which writes the data
- When button is clicked a POST request is made to
- The page hasn't changed yet, so we call
revalidatePath
from"next/cache"
, which tells next to revalidate the page and update the server components (without a full page reload). - We add a
loading.tsx
to display a skeleton
// dogs/[id]/edit/page.tsx
import kv from "@vercel/kv";
import { revalidatePath } from "next/cache";
interface Dog {
name: string;
image: string;
breed: string;
}
export default async function DogEditPage({
params,
}: {
params: { id: string };
}) {
// Fetch data
const key = `dogs:${params.id}`;
const dog = await kv.get<Dog>(key);
// OPTION 1. save and stay
async function upDog(formData: FormData) {
"use server";
// Mutate data
await kv.set(key, {
name: formData.get("title"),
image: formData.get("image"),
breed: formData.get("breed"),
});
// Revalidate
revalidatePath(`/dogs/${params.id}/edit`);
}
// OPTION 2. save and change page
async function upDogDeuce(formData: FormData) {
"use server";
// Mutate data
await kv.set(key, {
name: formData.get("title"),
image: formData.get("image"),
breed: formData.get("breed"),
});
// Revalidate
redirect(`/dogs/${params.id}`);
}
return (
<div className={styles.cardBody}>
<h2>Edit {dog?.name}</h2>
<form action={myAction}>
<label>Name</label>
<input name="title" type="text" defaultValue={dog?.name} />
<label>Image</label>
<input name="image" type="text" defaultValue={dog?.image} />
<label>Breed</label>
<input name="breed" type="text" defaultValue={dog?.breed} />
<button type="submit">Save and Continue</button>
<button formAction={upDogDeuce}>Save and Quit</button>
</form>
</div>
);
}
IMPORTANT: server actions only work on forms and things inside forms.
Trigger Server Actions From Client-Components
app/dogs/[id]/actions.ts
:
// app/dogs/[id]/actions.ts
"use server"
import kv from "@vercel/kv";
import { revalidatePath } from "next/cache";
export async function like(id: string) {
await kv.incr(`likes:${id}`);
revalidatePath(`/dogs/${id}`); // bust cache
}
export async function dilike(id: string) {
await kv.decr(`likes:${id}`);
revalidatePath(`/dogs/${id}`); // bust cache
}
Non-Blocking State Updates: useTransition
Now have a client-component that imports the server actions like
, dislike
and also useTransition
from react.
useTransition
is essential because it allows to update the state without blocking the UI i.e.:
- executes the server action
- next router will reload the server component and update the state
all in a single round trip
"use client"
import { like, dislike } from './actions';
import { useTransition } from 'react';
export default function Likes({ id }: { id: string; }) {
let [isPending, startTransition] = useTransition(); // non blocking state update
return (
<div>
<button onClick={
// the wrapping with startTransition is essential to update the UI
() => startTransition(() => like(id))
}>Like</button>
<button onClick={() => startTransition(() => dislike(id))}>Dislike</button>
</div>
)
}
A click will:
- call the server action (mutation),
- refetch the parent server-component's like count from the server
NOTE: server components void the need to use useEffect
in most of your components
Non-Blocking Optimistic State Updates: useOptimistic
It is a combination of useTransition
and useReducer
, and lets you update the client component to whatever you think the server is going to return.
And when the server returns the true value, the state (context) will sync up.
If it does fail, there should be some kind of error displayed when the state is rolled back.
"use client"
import { experimental_useOptmistic as useOptimistic } from "react";
import { like, dislike } from "./actions";
export default function Likes({ likeCount, id }: any) {
const [optimisticLikes, addOptimisticLike] = updateOptimistic(
{ likeCount, sending: false },
(state, newLikeCount) => ({
...state,
likeCount: newLikeCount,
sending: true,
})
);
return (
<div>
<div>
Optimistic Likes: {optimisticLikes.likeCount}
{optimisitcLikes.sending} ? " Sending..." : ""
</div>
<button onClick={
// We locally update without caring for what the outcome of await like(id) is
async () => {
addOptimisticLike(optimisticLikes.likeCount + 1);
await like(id);
};
}>Like</button>
<button onClick={
// We locally update without caring for what the outcome of await like(id) is
async () => {
addOptimisticLike(optimisticLikes.likeCount - 1);
await dislike(id);
};
}>Dislike</button>
</div>
);
}