next-js
Server components vs Client Components
Server components cannot be nested inside client components except through a trick where you render them as children, like so:
- Make the client component you want to render accept a
childrenprop - Render the server component as a child of the client component.
export const App = () => {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}
File-based routing
File based routing on NextJS makes route names based on folders, and renders page content with the page.tsx. Besides that, there are several other special files in NextJS that live within a route folder:
loading.tsx: loading placeholder that is displayed while thepage.tsxis in the process of rendering.layout.tsx: a layout that wraps thepage.tsx, only rendered once.template.tsx: a layout that wraps thepage.tsx, with the ability to rerender.loading.tsx: Loading UI for a segmenterror.tsx: Error handling UI for a segmentnot-found.tsx: 404 UI for a segment.
opting out of routing
To opt a folder and all subfolders and files in that folder out of routing, you can prefix the foldername with an underscore, like app/_lib.
You can also create route groups that opt out of routing just at the folder level br wrapping the folder name in parentheses, like so: (marketing). This is useful for grouping related content together.
examples
app/page.tsx: Renders content at the/routeapp/(auth)/signin/page.tsx: Renders content at the/signinrouteapp/(auth)/signout/page.tsx: Renders content at the/signoutroute
layout
This is what a typical layout page will look like:
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
{/* Layout UI */}
{/* Place children where you want to render a page or nested layout */}
<main>{children}</main>
</body>
</html>
)
}
error.tsx
This is what an error.tsx would look like, which would catch any error produced in a page component, accept that error as a prop, and display a UI based on it.
Here are the 2 props you can accept:
error: the error that caused the page to break.reset: a function that when invoked, tries rerendering the page.
"use client"
export default function Error({error, reset}) {
return (
<div>
<p>{e.message}</p>
<button onClick={reset}>Try again</button>
</div>
)
}
Route param pages
You can create dynamic file-based routing with route params by using square brackets around the folder name, like using app/[id]/page.tsx to render content according to the /:id dynamic route.
When dealing with route param pages, we can access the route params like so:
import { notFound } from 'next/navigation'
export default async function IssuePage({
params,
}: {
params: Promise<{ id: string }>
}) {
// 1. get route params
const { id } = await params
// 2. render rest of page
if (!listOfValidIds.includes(id)) {
return notFound()
}
return // ...
}
NOTE
The params prop is a promise, so you have to await it.
By default, since NextJS has no idea which params will be passed to the dynamic route param page, it dynamically renders to page. To bypass this, you can choose a subset of route parameters to prebuild and generate static pages for, like so, by exporting the async generateStaticParams() method.
export async function generateStaticParams() {
const posts = await fetch('https://api.vercel.app/blog', {
cache: 'force-cache',
}).then((res) => res.json())
return posts.map((post: Post) => ({
id: String(post.id),
}))
}
export default async function Page({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const post = await getPost(id)
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}
Search Param Pages
You can also access the query parameters on a route by destructuring the searchParams prop on a page component and type hinting it.
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
const filters = (await searchParams).filters
}
- Use the
searchParamsprop when you need search parameters to load data for the page (e.g. pagination, filtering from a database). - Use
useSearchParamswhen search parameters are used only on the client (e.g. filtering a list already loaded via props).
Search Param Helper Validator class
import { redirect } from "next/navigation";
import { z } from "zod";
export class NextJSSearchParams<T extends Record<string, any>> {
redirectWithPayload(url: string, payload: T) {
redirect(`${url}?payload=${encodeURIComponent(JSON.stringify(payload))}`);
}
getSearchParams(searchParams: T) {
return searchParams;
}
}
export type ParamsInferType<T extends Record<string, any>> = {
payload: T;
};
export type SearchParamsType = Promise<{
payload: string;
}>;
export class NextJSSearchParamsZod<T extends z.ZodObject<any>> {
constructor(private schema: T) {}
redirectWithPayload(url: string, payload: z.infer<T>) {
redirect(`${url}?payload=${encodeURIComponent(JSON.stringify(payload))}`);
}
getSearchParams(searchParams: { payload: string }) {
try {
const data = JSON.parse(searchParams.payload) as z.infer<T>;
return this.schema.safeParse(data).data as z.infer<T>;
} catch (e) {
return undefined;
}
}
isValidSearchParams(searchParams: { payload: string }) {
try {
const data = JSON.parse(searchParams.payload);
return this.schema.safeParse(data).success;
} catch (e) {
return false;
}
}
createRouteGuard(
searchParams: {
payload: string;
},
fallbackUrl: string
) {
if (!this.isValidSearchParams(searchParams)) {
redirect(fallbackUrl);
}
}
}
Parallel Routes
Parallel Routes in Next.js enable the simultaneous or conditional rendering of multiple pages or components within the same layout. This is especially beneficial for sections of an application that require dynamic content changes without navigating away from the page, like social media feeds or analytics dashboards.
Here are the key benefits of parallel routes:
- Simultaneous Rendering: Multiple components or pages can be rendered at the same time within the same layout, enhancing the user experience by providing a composite view of related content.
- Conditional Rendering: Depending on the application state or user actions, specific components can be rendered, allowing for a highly responsive and interactive interface.
NOTE
The main benefit of using parallel routes is reusability. You don't have to rewrite the logic for fetching each individual page and rebuilding them from scratch, you can just use them as components, gaining all the benefits that comes with being a page component, like SSR.
Next.js manages the active state for each slot, ensuring that the content within each slot is appropriate to the current navigation context.
Here is the behavior of the two navigation types when using route slots:
- Soft Navigation: Changes content within a slot during client-side navigation without reloading the entire page. This maintains the state of other slots, ensuring a seamless user experience.
- Hard Navigation: On a full page reload, Next.js will attempt to recover the state of slots. If it cannot determine the active state for a slot, a default component (usually
default.js) is rendered, or a 404 error if no default exists.
implementing parallel routes
You create a parallel route folder by prefixing with a @. The folder structure can be like so:
app
dashboard
@events
page.tsx
@rsvps
page.tsx
layout.tsx
page.tsx
In each route slot folder, you should have these two special components:
page.tsx: what gets rendered for that routedefault.tsx: what gets rendered if that route does not match after a hard reload
Then in the layout that is the parent for the route slots, you can accept each route slot as a prop:
// /app/dashboard/layout.tsx
'use client'
import Shell from '@/components/Shell'
import { usePathname } from 'next/navigation'
const DashboardLayout = ({ children, rsvps, events }) => {
const path = usePathname()
return (
<Shell>
{path === '/dashboard' ? (
<div className="flex w-full h-full">
<div className="w-1/2 border-r border-default-50">{rsvps}</div>
<div className="w-1/2 flex flex-col">
<div className="border-b border-default-50 w-full h-1/2">
{events}
</div>
<div className="w-full h-1/2">{children}</div>
</div>
</div>
) : (
<div>{children}</div>
)}
</Shell>
)
}
export default DashboardLayout
Miscellaneous
<Image>
The <Image> tag in NextJS provides lazy loading, preventing layout shift, and optimizing to webp. There are two different ways you can use it:
Local images
When you load local image paths to the src= attribute of the <Image> component, NextJS can automatically infer the width, height, and placeholder blur.
import Image from 'next/image'
import profilePic from './me.png'
export default function Page() {
return (
<Image
src={profilePic}
alt="Picture of the author"
// width={500} automatically provided
// height={500} automatically provided
// blurDataURL="data:..." automatically provided
// placeholder="blur" // Optional blur-up while loading
/>
)
}
remote images
When loading remote images from base64 data or any url, you need to provide the width= and height= props to prevent layout shift.
import Image from 'next/image'
export default function Page() {
return (
<Image
src="https://s3.amazonaws.com/my-bucket/profile.png"
alt="Picture of the author"
width={500}
height={500}
/>
)
}
To safely allow images from remote servers, you need to define a list of supported URL patterns in next.config.js. Be as specific as possible to prevent malicious usage.
import { NextConfig } from 'next'
const config: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 's3.amazonaws.com',
port: '',
pathname: '/my-bucket/**',
search: '',
},
],
},
}
export default config
Fonts
Next.js downloads font files at build time and hosts them with your other static assets. This means when a user visits your application, there are no additional network requests for fonts which would impact performance.
here is how you can load a google font:
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<body>{children}</body>
</html>
)
}
Here is how you can load a local font:
import localFont from 'next/font/local'
const myFont = localFont({
src: './my-font.woff2',
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={myFont.className}>
<body>{children}</body>
</html>
)
}
You can them use them in your CSS by referencing them as normal CSS variables:
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
Metadata
You can dynamically generate metadata for each route based like so in a page.tsx:
import type { Metadata, ResolvingMetadata } from 'next'
type Props = {
params: Promise<{ id: string }>
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}
export async function generateMetadata(
{ params, searchParams }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
const slug = (await params).slug
// fetch post information
const post = await fetch(`https://api.vercel.app/blog/${slug}`).then((res) =>
res.json()
)
return {
title: post.title,
description: post.description,
}
}
export default function Page({ params, searchParams }: Props) {}
Routing
<Link>
The <Link /> component is used for client-side soft routing that basically changes only the content that needs to change. It uses soft navigation:
- soft navigation: navigation like react router - only changing the DOM to give the illusion of a page navigation instead of performing an HTTP request to fetch the new route.
- hard navigation: Performing a HTTP request to fetch the content under a new route.
import Link from 'next/link'
export const Signout = () => {
return (
<Link href="/signup">
<Button size="lg">Get Started</Button>
</Link>
)
}
useRouter()
import { useRouter } from 'next/navigation'
const router = useRouter()
router.back(): goes back to the previous routerouter.refresh(): refreshes the page (does a hard refresh)
Navigation functions
Here are the navigation functions you can import from next/navigation:
redirect(route: string): redirects to the given routenotFound(): renders 404 not found page
WARNING
There is a known bug that if you invoke redirect() within a try-catch block, it will error out. So only invoke redirection outside of a try catch.
import { redirect } from 'next/navigation'
import IssueForm from './IssueForm'
import { getCurrentUser } from '@/lib/dal'
const NewIssue = async () => {
const user = await getCurrentUser()
if (!user) {
redirect('/signin')
}
return <IssueForm userId={user.id} />
}
export default NewIssue
Rendering strategies
CSR
Client-side rendering is the traditional react way of using javascript to build out the page.
In next.js, you can opt into this by using the "use client" directive on any component, making it client-side only.
// components/UpvoteButton.tsx
"use client"; // 👈 Marks this as a Client Component
import { useState } from "react";
export default function UpvoteButton() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Upvote ({count})
</button>
);
}
NOTE
"Client Components" in Next.js are pre-rendered once on the server for the initial HTML, then "hydrated" in the browser to become interactive, so if you want to make pages static, use client-side data fetching to let the page be static but take advantage of hydration.
SSR
SSR is the default in nextjs, where async page components means that the entire page is rendered server-side (except for nested client components) and is dynamic, meaning it fetches the data each and every time.
- Concept: The page is rendered on the server on demand for every single request.
- This happens automatically if you access runtime information (Cookies, Headers, Search Params).
- This can also happen when you use dynamicIO and wrap an async component within suspense instead of caching it with
"use cache"
- Use Case: Personalized dashboards, pages behind authentication, real-time data.
SSG
Static site generation involves building out pages ahead of time, which is a technique we can use on route param routes like /websites/[id].
We do this by returning the params from generateStaticParams() on a dynamic route param route, and then for all params, nextjs will build out a version of that page for each param.
export async function generateStaticParams() {
const posts = await prisma.post.findMany();
// Returns [{ id: '1' }, { id: '2' }]
// Next.js will build HTML for these specific pages at build time.
return posts.map((post) => ({ id: String(post.id) }));
}
export default async function PostPage({ params }) {
// ... renders static post
}
ISR
Use nextjs caching to only revalidate or invalidate the cache for a page when the data the page uses has been mutated. You yourself need to manually set rules to invalidate the cache.
PPR
Partial prerendering is new in nextjs and it is the "Holy Grail" of rendering. It prevents making a whole route/page dynamic if it has a <Suspense> somewhere in its component tree.
- concept: combines SSG and Dynamic Rendering in the same page. You keep the outer shell (Navbar, Footer, Main Content) static, while isolating dynamic parts (like a "Recently Viewed" sidebar) in holes that are filled in dynamically.
- The Problem it Solves: Previously, if one tiny child component inside a page was asynchronous (called
fetch(),cookies(), etc.), the entire page became Dynamic (SSR). With PPR, only that component is Dynamic.
Here's an example:
import { Suspense } from "react";
import { cookies } from "next/headers";
// 1. The Page itself remains STATIC (SSG)
export default function Page() {
return (
<main>
<h1>Static Blog Content</h1>
<p>This part is pre-rendered at build time.</p>
{/* 2. Boundary: Isolate the dynamic part */}
<Suspense fallback={<div>Loading personal history...</div>}>
<RecentlyViewed />
</Suspense>
</main>
);
}
// 3. This component accesses cookies, so it renders dynamically (SSR)
async function RecentlyViewed() {
const cookieStore = cookies(); // 👈 This reads runtime info
const history = getHistory(cookieStore);
return <div>You recently viewed: {history}</div>;
}
Concept: Historically, you had to choose a rendering strategy for your entire page.
- Static (SSG): Fast, cached at the edge, but cannot handle personalized data (e.g., "Welcome, User").
- Dynamic (SSR): Personalized, but slower because the server must calculate the entire page before sending anything.
The "Poisoning" Effect: In Next.js, if you use a dynamic API (like cookies() or headers()) anywhere in your component tree, the entire route switches to Dynamic Rendering. This means even your static footer and logo wait for the database call to finish before loading.
Concept: PPR uses React Suspense boundaries to isolate dynamic code. Next.js detects these boundaries at build time.
-
The Shell: Everything outside the Suspense boundary is pre-rendered as static HTML (the "Shell").
-
The Holes: Everything inside the Suspense boundary is treated as dynamic. It is replaced with a fallback (loading state) in the initial HTML.
-
Runtime: When a user visits, they get the Shell immediately (from the Edge). The dynamic parts run on the server and stream in parallel.
Therefore, the new best practice is the following:
- Always put your dynamic code
cookies(),fetch(), etc. in an async component that is nested farther down the page level rather than wrapping the entire page, so you can take advantage of PPR and leave some parts of the page static while you leave other parts dynamic. - In next 16, all you have to do is use
<Suspense>to wrap dynamic asyn components, and then you automatically opt into partial prerendering
Next 15
In next 15, you enable it in the next config:
nextConfig.experimental.ppr = true
and then specify ppr for a page like so:
export const experimental_ppr = true;
Next 16
Partial prerendering is now baked into nextjs 16 and thus is the default.
Server Actions
Server actions are syntactic sugar in NextJS that from a normal javascript function running server-side, creates a POST API route behind the scenes that is automatically fetched when a user invokes the server action.
There are three different ways to use server actions:
- form action: The reason why they are called Server Actions is because you can pass the server action function to the
action=attribute on a form, which will then trigger the server action with theFormDatawhen the form is submitted. - button action: On any button inside a form, you can pass in a server action function to the
formAction=attribute. - invoke it manually: Simply invoke it manually, passing in whatever params you want and returning whatever you want.
Here are the benefits of server actions over normal API routes you can make in NextJS:
- You get type safety since server actions are just normal javascript functions you import into your client components.
- You don't have to write cumbersome
fetch()requests. Just set theaction=prop on a form and set it to the server action function, and you're done.
Server actions as form actions
Server actions are normal javascript functions running server-side that you define. To define a server action, use the "use server" directive at the top of the file containing all your actions code.
What the "use server" directive does is that it transforms what would otherwise be a normal JS function running in the node runtime into an HTTP route handler. Thus there are two important things that must be understood here:
- You CAN ONLY use server actions in client components, and attach them to the
action=prop on a<form>element. - You CANNOT use server actions in server components, since they are essentially HTTP route handlers and do not work outside of that context.
step 1: create the form action
- In some arbitrarily named file like
actions.ts, put a"use server"directive at the top of the file.
'use server'
export async function signIn(formData: FormData): Promise<ActionResponse> {
// Extract data from form
const data = {
email: formData.get('email') as string,
password: formData.get('password') as string,
}
// 1. check email and password against database
redirect("/dashboard")
}
- In your form, to actually populate the form data, you need to put
name=attributes on your input elements. - Pass the action function to the
action=prop on a<form>tag to connect the server action as the handler for the form.
This was the most basic way to use server actions, preventing forms from having to become client components.
// 1. import server action called "action"
export default function MyForm() {
return (
<form action={action}>
{ /* input elements here ... */}
</form>
)
}
useActionState hook
Then the next step is to actually use the server action in your form submission by following these two steps:
- Use the
useActionState()hook, passing in your server action, which returns the form action to pass in to theaction=prop, a loading state, and state representing the return result from the server action. - Define more what you want to happen in the callback to the hook.
Here is how the basic usage of the hook works:
import { useActionState } from 'react'
const [state, formAction, isPending] = useActionState(cb, initialState)
Here are the arguments you pass in:
initialState: the initial state of the server action response. Thestatevariable will have the exact same formcb: a callback that takes in two arguments: the previous state and the form data, and you must return the new state in the callback.
Here are the variables that are returned from the hook:
state: the current state of the server action responseformAction: the action object you pass into theaction=prop of the form. Remember that callback you passed into theuseActionState()hook? Yeah, that callback will get triggered when the form gets submitted, all connected through passing theformActionto theaction=prop of that form.
'use client'
import { useActionState } from 'react'
import { useRouter } from 'next/navigation'
import Button from '@/app/components/ui/Button'
import {
Form,
FormGroup,
FormLabel,
FormInput,
FormError,
} from '@/app/components/ui/Form'
import Link from 'next/link'
import toast from 'react-hot-toast'
import { signIn, ActionResponse } from '@/app/actions/auth'
const initialState: ActionResponse = {
success: false,
message: '',
errors: undefined,
}
export default function SignInPage() {
const router = useRouter()
// Use useActionState hook for the form submission action
const [state, formAction, isPending] = useActionState<
ActionResponse,
FormData
>(async (prevState: ActionResponse, formData: FormData) => {
try {
const result = await signIn(formData)
// Handle successful submission
if (result.success) {
toast.success('Signed in successfully')
router.push('/dashboard')
router.refresh()
}
return result
} catch (err) {
return {
success: false,
message: (err as Error).message || 'An error occurred',
errors: undefined,
}
}
}, initialState)
return (
//... rest of the component
<Form action={formAction} className="space-y-6">
{state?.message && !state.success && (
<FormError>{state.message}</FormError>
)}
<FormGroup>
<FormLabel htmlFor="email">Email</FormLabel>
<FormInput
id="email"
name="email"
type="email"
autoComplete="email"
required
disabled={isPending}
aria-describedby="email-error"
className={state?.errors?.email ? 'border-red-500' : ''}
/>
{state?.errors?.email && (
<p id="email-error" className="text-sm text-red-500">
{state.errors.email[0]}
</p>
)}
</FormGroup>
<FormGroup>
<FormLabel htmlFor="password">Password</FormLabel>
<FormInput
id="password"
name="password"
type="password"
autoComplete="current-password"
required
disabled={isPending}
aria-describedby="password-error"
className={state?.errors?.password ? 'border-red-500' : ''}
/>
{state?.errors?.password && (
<p id="password-error" className="text-sm text-red-500">
{state.errors.password[0]}
</p>
)}
</FormGroup>
<div>
<Button type="submit" className="w-full" isLoading={isPending}>
Sign in
</Button>
</div>
</Form>
//... rest of the component
)
}
Here's a simpler example:
const [state, formAction, isPending] = useActionState<
ActionResponse,
FormData
>(
// first argument is callback
async (prevState, formData) => {
const response = await addNoteAction(formData);
if (response.success) {
router.refresh();
return {
success: true,
message: "Note added successfully",
error: "",
};
}
return response;
},
// 2nd argument is initial state
{
success: false,
message: "",
error: "",
}
);
Here's another example:
'use client'
import { useActionState } from 'react'
import { useRouter } from 'next/navigation'
import { Issue, ISSUE_STATUS, ISSUE_PRIORITY } from '@/db/schema'
import Button from './ui/Button'
import {
Form,
FormGroup,
FormLabel,
FormInput,
FormTextarea,
FormSelect,
FormError,
} from './ui/Form'
import { createIssue, ActionResponse } from '@/app/actions/issues'
interface IssueFormProps {
issue?: Issue
userId: string
isEditing?: boolean
}
const initialState: ActionResponse = {
success: false,
message: '',
errors: undefined,
}
export default function IssueForm({
issue,
userId,
isEditing = false,
}: IssueFormProps) {
const router = useRouter()
// Use useActionState hook for the form submission action
const [state, formAction, isPending] = useActionState<
ActionResponse,
FormData
>(async (prevState: ActionResponse, formData: FormData) => {
// Extract data from form
const data = {
title: formData.get('title') as string,
description: formData.get('description') as string,
status: formData.get('status') as
| 'backlog'
| 'todo'
| 'in_progress'
| 'done',
priority: formData.get('priority') as 'low' | 'medium' | 'high',
userId,
}
try {
// Call the appropriate action based on whether we're editing or creating
const result = isEditing
? await updateIssue(Number(issue!.id), data)
: await createIssue(data)
// Handle successful submission
if (result.success) {
router.refresh()
if (!isEditing) {
router.push('/dashboard')
}
}
return result
} catch (err) {
return {
success: false,
message: (err as Error).message || 'An error occurred',
errors: undefined,
}
}
}, initialState)
const statusOptions = Object.values(ISSUE_STATUS).map(({ label, value }) => ({
label,
value,
}))
const priorityOptions = Object.values(ISSUE_PRIORITY).map(
({ label, value }) => ({
label,
value,
})
)
return (
<Form action={formAction}>
{state?.message && (
<FormError
className={`mb-4 ${
state.success ? 'bg-green-100 text-green-800 border-green-300' : ''
}`}
>
{state.message}
</FormError>
)}
<FormGroup>
<FormLabel htmlFor="title">Title</FormLabel>
<FormInput
id="title"
name="title"
placeholder="Issue title"
defaultValue={issue?.title || ''}
required
minLength={3}
maxLength={100}
disabled={isPending}
aria-describedby="title-error"
className={state?.errors?.title ? 'border-red-500' : ''}
/>
{state?.errors?.title && (
<p id="title-error" className="text-sm text-red-500">
{state.errors.title[0]}
</p>
)}
</FormGroup>
<FormGroup>
<FormLabel htmlFor="description">Description</FormLabel>
<FormTextarea
id="description"
name="description"
placeholder="Describe the issue..."
rows={4}
defaultValue={issue?.description || ''}
disabled={isPending}
aria-describedby="description-error"
className={state?.errors?.description ? 'border-red-500' : ''}
/>
{state?.errors?.description && (
<p id="description-error" className="text-sm text-red-500">
{state.errors.description[0]}
</p>
)}
</FormGroup>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormGroup>
<FormLabel htmlFor="status">Status</FormLabel>
<FormSelect
id="status"
name="status"
defaultValue={issue?.status || 'backlog'}
options={statusOptions}
disabled={isPending}
required
aria-describedby="status-error"
className={state?.errors?.status ? 'border-red-500' : ''}
/>
{state?.errors?.status && (
<p id="status-error" className="text-sm text-red-500">
{state.errors.status[0]}
</p>
)}
</FormGroup>
<FormGroup>
<FormLabel htmlFor="priority">Priority</FormLabel>
<FormSelect
id="priority"
name="priority"
defaultValue={issue?.priority || 'medium'}
options={priorityOptions}
disabled={isPending}
required
aria-describedby="priority-error"
className={state?.errors?.priority ? 'border-red-500' : ''}
/>
{state?.errors?.priority && (
<p id="priority-error" className="text-sm text-red-500">
{state.errors.priority[0]}
</p>
)}
</FormGroup>
</div>
<div className="flex justify-end gap-2 mt-6">
<Button
type="button"
variant="ghost"
onClick={() => router.back()}
disabled={isPending}
>
Cancel
</Button>
<Button type="submit" isLoading={isPending}>
{isEditing ? 'Update Issue' : 'Create Issue'}
</Button>
</div>
</Form>
)
}
Other ways to use server actions
Since server actions are just syntactic sugar for fetching an API route you make, you can invoke them anywhere, in any event handler like onClick= or as a result of a form submission with the action= prop.
button with formAction=
If you want to launch a server action with a button click instead of doing stuff with a form, you can do it by passing a server action to the formAction= prop or the onClick= to the <button> component.
- Create a server action that does not take any form data arguments:
'use server'
export async function createPost() {}
- Pass in the server action to the
formAction=prop of the button that when clicked, you want it to trigger the server action:
'use client'
import { createPost } from '@/app/actions'
export function Button() {
return <button formAction={createPost}>Create</button>
}
Button with onClick=
You can create a server action that accepts any arguments and returns data in any form, and then you can manually call that server action as a normal javascript function inside an event handler to data fetch it in a useEffect.
For example, you can use a server action in an onClick= prop like so:
`
export async function deleteNoteAction(id: number) {
"use server";
notesModel.deleteNote(id);
notesCacher.uncache();
}
"use client";
import { deleteNoteAction } from "@/actions/actions";
import { TrashIcon } from "@heroicons/react/24/outline";
import React from "react";
import { useRouter } from "next/navigation";
export const TrashButton = ({ id }: { id: number }) => {
const router = useRouter();
return (
<button
className="text-sm text-gray-500 hover:text-red-500 cursor-pointer"
onClick={async () => {
await deleteNoteAction(id);
router.refresh();
}}
>
<TrashIcon className="w-4 h-4" />
</button>
);
};
Things you can do in server actions
Redirecting users
You can do stuff like redirecting a user in a server action:
'use server'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
// Update data
// ...
redirect('/posts')
}
IMPORTANT
You CANNOT use the redirect() function within a try-catch block. You can only use it outside.
useFormStatus
There is the useFormStatus hook from React that allows you to access form loading state and thus change UI based on that state.
The only rule is that whatever component you use this hook in must be rendered somewhere inside a form.
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton() {
const { pending } = useFormStatus()
return (
<button disabled={pending} type="submit">
Sign Up
</button>
)
}
Here are the properties on the object returned from the useFormStatus() hook:
datapending
useTransition
useTransition() is a react 19 hook that lets you perform blocking operations like server actions while keeping the UI snappy.
- Non-blocking updates: Marking an update as a transition prevents it from blocking interaction with the UI.
- Handling multiple transitions: Currently, React batches multiple transitions, although this may change in future releases.
- Limitations: Transitions are not suitable for controlling text inputs and must be synchronous.
// /actions/events.ts
'use server'
import { db } from '@/db/db'
import { events } from '@/db/schema'
import { delay } from '@/utils/delay'
import { getCurrentUser } from '@/utils/users'
import randomName from '@scaleway/random-name'
export const createNewEvent = async () => {
await delay(1000)
const user = await getCurrentUser()
await db.insert(events).values({
startOn: new Date().toUTCString(),
createdById: user.id,
isPrivate: false,
name: randomName('event', ' '),
})
}
// /components/Nav.tsx
'use client'
import { Input } from '@nextui-org/react'
import { createNewEvent } from '@/actions/events'
import { Button, Tooltip } from '@nextui-org/react'
import { CirclePlus } from 'lucide-react'
import { useTransition } from 'react'
const Nav = () => {
const [isPending, startTransition] = useTransition()
const handleClick = () => {
startTransition(() => {
createNewEvent()
})
}
return (
<nav className="h-[65px] border-b border-default-50 flex items-center px-6 gap-4">
<div>
<Tooltip content="New Event">
<Button
isIconOnly
variant="ghost"
size="sm"
isLoading={isPending}
onClick={handleClick}
>
<CirclePlus size={16} />
</Button>
</Tooltip>
</div>
<div className="w-1/2">
<Input size="sm" variant="faded" placeholder="search" />
</div>
</nav>
)
}
export default Nav
Caching
dynamic vs static in nextjs
In NextJS 15, caching is heavily improved. To setup caching on the canary version of nextjs, do the following and first install the canary version with npm install next@canary. You then need to enable dynamicIO.
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
dynamicIO: true,
},
}
export default nextConfig
If in next 16, this feature is now stable and you can enable it via cacheComponents flag in the next config:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true
};
export default nextConfig;
Caching is done on the page level in the page.tsx component. NextJS forces you to explicitly define if you want a static cached page or a dynamic page.
These behaviors sometimes happen by default:
- static by default: A page component will be static by default if it is not asynchronous, meaning it does no data fetching or does not fetch anything like cookies or search parameters. All it does is return HTML.
- should be dynamic: A page component is most likely dynamic if it is asynchronous or it uses
fetch(),cookies(),headers(), orsearchParams()
NOTE
You MUST define either caching or dynamic behavior if any server component rendered in your page is asynchronous. NextJS will throw an error until you decide whether to make the page dynamic with <Suspense> or implement some sort of caching strategy with "use cache"
"use cache"
The "use cache" directive in NextJS 15 is the new way to opt into caching server-side.
The "use cache" directive when placed at the top of the file caches all the exports from the file, and when placed as the first line in a function, it caches the return value from that function, memoizing it. And when used in a page component, it caches the entire page.
To make a page cached forever with the cache-first strategy, use the "use cache" directive at the top of the page.tsx.
There are three ways you can cache with the "use cache" directive.
- Caching at the page level with
page.tsx - Caching a React server component
- Caching a function's return value
// File/page level
'use cache'
export default async function Page() {
// ...
}
// Component level
export async function MyComponent() {
'use cache'
return <></>
}
// Function level
export async function getData() {
'use cache'
const data = await fetch('/api/data')
return data
}
IMPORTANT
It is extremely important to understand that "use cache" caches server-side, making it a global cache that affects all clients. For client-side or per-request caching, use the cache() function from React.
Revalidation techniques
By default, the "use cache" directive caches the page/component/function forever, revalidating the cache every 15 minutes. You can change how often the cache revalidates or gets invalidated through three main caching techniques in NextJS:
- tag based caching: use
cacheTag(name)andrevalidateTag(name)functions to manually cache and invalidate functions. - path based caching: use
revalidatePath(route)to revalidate the cache for a page, which is useful if you used"use cache"at the top of a page. - cache life: You can manually change the cache life of a page in nextjs from being infinitely cached to something different using the
cacheLife()function.
tag based revalidation
To implement manual revalidation, you can used a tags-based approach that lets you revalidate content connected to a specific tag:
cacheTag(tag: string): caches the function/component under the specified tagrevalidateTag(tag: string): from the specified tag, removes the function from the cache. However, this has stale while revalidate behavior, meaning that it still shows the initial cached version before getting the fresh data back.updateTag(tag: string): invalidates the cache of the function that the tag references. However, it immediately serves fresh data.
page based revalidation
You can also revalidate entire paths, if you cache at the page level:
revalidatePath(path: string): revalidates a page cached with"use cache"
cache life
There are three components to cache life that you should understand before invoking this function:
- revalidate time: the period of time data stays cached until checking for revalidation server-side
- stale time: the client side cache time, duration the client should cache a value before falling back to the server. (Obviously, data should only be cached client side to store the data on their end instead of the server)
- expiration time: sets the time to wait before the cache will get deleted, after which no caching will take place.
Here are the default string values you can pass into the cacheLife() function, which are presets of combinations of all three components. By default, stale time is infinite, cache never expires, and is revalidated every 15 minutes.
"seconds": Revalidated every second, cache only lasts 1 second."minutes": Revalidated every 5 minutes, cache only lasts 5 minute.
"use cache" // 1. must cache page with 'use cache' directive to use cache life
import {unstable_cacheLife as cacheLife } from "next/cache"
export default async function Page() {
cacheLife('hours')
return <><>
}
You can also add different combinations manually by passing in an object of those three time components into the cacheLife() function:
stale: how long the client side cache should store in seconds.revalidate: the cache revalidation interval length in seconds.expire: how long after the cache should expire in seconds.
import { cacheLife } from 'next/cache'
async function getData() {
'use cache'
cacheLife({
stale: 300, // 5 minutes - client uses cache without checking
revalidate: 900, // 15 minutes - server revalidates in background
expire: 3600 // 1 hour - cache completely expires
})
return await fetch('/api/data').then(r => r.json())
}
'use cache'
cacheLife('seconds') // Ultra-fresh data
// stale: 30s, revalidate: 1s, expire: 1m
cacheLife('minutes') // Rapidly changing data
// stale: 5m, revalidate: 1m, expire: 1h
cacheLife('hours') // Moderately fresh data
// stale: 5m, revalidate: 1h, expire: 1d
cacheLife('days') // Stable content
// stale: 5m, revalidate: 1d, expire: 1w
cacheLife('weeks') // Very stable content
// stale: 5m, revalidate: 1w, expire: 30d
cacheLife('max') // Nearly immutable
// stale: 5m, revalidate: 30d, expire: never
cacheLife('default') // Balanced
// stale: 5m, revalidate: 15m, expire: never
1) Caching at the page level
When you cache at the page level, you are telling NextJS to statically prerender that page, and to only dynamically regenerate by revalidating the cache manually, which you can do by revalidating the path or a tag.
WARNING
Since "use cache" has buildtime behavior here, you CANNOT use dynamic data associated with requests like cookies() or headers().
Remember to implement revalidation with revalidatePath() if you choose to cache at the page level, which invalidates all caches used in a route.
- Call
"use cache"at the page level - In some server action or server side function that mutates backend data, call
revalidatePath(path)and pass in the route your want to invalidate the cache for.
'use server'
import { revalidatePath } from 'next/cache'
export async function updateUserProfile(userId: string) {
await db.users.update(userId)
revalidatePath('/profile') // Single page
revalidatePath(`/users/${userId}`) // Dynamic route
revalidatePath('/blog', 'layout') // All pages in /blog
}
2) Caching at the component level
Caching at the component level essentially memoizes the props and any computations that happen inside the component. The cached value will be returned as long as all the serialized props are the same across each call.
export async function Bookings({ type = 'haircut' }: BookingsProps) {
'use cache'
async function getBookingsData() {
const data = await fetch(`/api/bookings?type=${encodeURIComponent(type)}`)
return data
}
return //...
}
interface BookingsProps {
type: string
}
The one exception to the serialized props is children, which can be different each time.
3) Caching at the function level.
You can cache async function calls with the "use cache" directive to cache the function's return value.
- To opt into caching the return values of functions, use the
"use cache"directive at the first line of the function body. - To invalidate the cache for the function, first attach a specific reference name to that function by invoking
cacheTag(some_tag)within the function body, and then callrevalidateTag(some_tag)to invalidate the cache of the function referenced by that tag.
You have two methods for invalidation:
revalidateTag(tag, cachelife): performs stale while revalidate with some cache life- use case: for content where slight delays are acceptable, like blogs or product catalogs.
updateTag(tag): invalidates the cache and immediately serves fresh data- use case: for immediate consistency, like user-created content where the user needs to immediately see the data they created.
A basic technique is to cache data fetching functions and to reset the cache when a new resource is created or updated:
import { unstable_cacheTag as cacheTag } from 'next/cache'
const getIssues = async () => {
'use cache'
cacheTag('issues')
// ...rest
}
import { revalidateTag } from 'next/cache'
export const createIssue = async () => {
// after mutating data by adding issues, revalidate it
revalidateTag('issues')
}
revalidateTag(tag)
'use server'
import { revalidateTag } from 'next/cache'
export async function updateArticle(articleId: string) {
await db.articles.update(articleId)
// Mark as stale - users see old content while it revalidates
revalidateTag(`article-${articleId}`, 'max')
}
updateTag(tag)
'use server'
import { updateTag } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const post = await db.posts.create({
title: formData.get('title'),
content: formData.get('content'),
})
// Immediately expire - user must see new post now
updateTag('posts')
updateTag(`post-${post.id}`)
redirect(`/posts/${post.id}`)
}