Server Functions
Write functions that run on the server and call them straight from a form, then refresh the page with revalidatePath.
The Reading List can now read books on the server. To add a book, the browser has to send data the other way. In the Vite version you changed state in the browser. With a server, the usual answer is to build an API endpoint, call it with fetch, check the response, and then reload the list.
React has a shorter path. A Server Function is an async function that runs on the server, but that you can call from the browser as if it were a normal function. The framework creates the network request for you. When a Server Function is used as a form action or in a mutation, people also call it a Server Action.
Writing a Server Function
Create app/actions.ts and put the 'use server' directive at the very top. Every function exported from this file becomes a Server Function.
'use server';
import { revalidatePath } from 'next/cache';
import { saveBook } from '@/lib/books';
import type { FormState } from '@/lib/types';
export async function addBook(prevState: FormState, formData: FormData): Promise<FormState> {
const title = String(formData.get('title') ?? '').trim();
const author = String(formData.get('author') ?? '').trim();
if (!title || !author) {
return { error: 'Please enter both a title and an author.', message: null };
}
if (title.length > 200 || author.length > 200) {
return { error: 'Please keep the title and author under 200 characters.', message: null };
}
const book = await saveBook({ title, author });
revalidatePath('/');
return { error: null, message: `Added “${book.title}” to your list.` };
}FormState lives in lib/types.ts as { error: string | null; message: string | null }. The function:
- reads the fields from the form's FormData, the same object the browser builds when you submit a form;
- checks them and returns an error if they are not valid;
- saves the book through
saveBookfromlib/books.ts; - calls
revalidatePath('/')so the home page shows the new book (more on this below); - returns the new form state.
When the browser calls addBook, it sends a POST request with the form data to your server. Next.js runs the function there and sends back the return value. You never see the request, but it is there, and that matters for security.
Calling it from a form
On Day 6 you passed a function to useActionState and gave the returned action to <form action>. A Server Function slots straight into the same place. The form is interactive, so it is a Client Component, and it imports addBook like any other function:
'use client';
import { useActionState } from 'react';
import { addBook } from '@/app/actions';
import type { FormState } from '@/lib/types';
const initialState: FormState = { error: null, message: null };
export function AddBookForm() {
const [state, formAction, isPending] = useActionState(addBook, initialState);
return (
<form action={formAction}>
<label htmlFor="title">Title</label>
<input id="title" name="title" required />
<label htmlFor="author">Author</label>
<input id="author" name="author" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Adding...' : 'Add book'}
</button>
<p role="status">{state.error ?? state.message}</p>
</form>
);
}useActionState calls addBook(previousState, formData), which is why addBook takes the previous state as its first parameter. Whatever it returns becomes the new state. While the request is in flight, isPending is true. After the action finishes, React resets the form's inputs.
You can't run a server in this page, so the sandbox below fakes one. actions.js and db.js play the server, with a short delay, and a read-only fake-next.js plays Next.js. The form code is the same as in your project.
Try submitting with an empty author to see the error. Then add a real book and watch the button change to "Adding..." while the pretend server works.
Two things are different in a real Next.js project. actions.js starts with 'use server', so its code stays on the server and the browser only gets a reference that sends a request. And you don't write anything like fake-next.js: revalidatePath comes from next/cache.
Refreshing the page with revalidatePath
After saveBook runs, the data on the server has changed, but the browser is still showing the page it rendered earlier. Next.js does not re-render pages after every Server Function, because it can't know which pages your change affects.
revalidatePath('/') tells it. It means: "the data behind the / page changed, so throw away any stored copy and render it again". When you call it inside a Server Function, Next.js sends the fresh page back in the same response as the function's result, and the list updates without a reload.
What does revalidatePath('/') do when called inside a Server Function?
Passing an id with bind
Each book card needs buttons to change the status and to delete. Those Server Functions need to know which book. Here they are, next to addBook:
export async function cycleStatus(id: string) {
await cycleBookStatus(id);
revalidatePath('/');
}
export async function deleteBook(id: string) {
await removeBook(id);
revalidatePath('/');
}A form action receives only the form data, so you can't write action={deleteBook(book.id)}: that would call the function during rendering. Instead, use the JavaScript method bind, which creates a new function with the first argument already filled in:
import Link from 'next/link';
import { cycleStatus, deleteBook } from '@/app/actions';
import { statusLabels, type Book } from '@/lib/types';
export function BookCard({ book }: { book: Book }) {
return (
<article className="book-card">
<h3>
<Link href={`/books/${book.id}`}>{book.title}</Link>
</h3>
<p className="author">by {book.author}</p>
<p className={`badge badge-${book.status}`}>{statusLabels[book.status]}</p>
<form action={cycleStatus.bind(null, book.id)}>
<button type="submit">Next status</button>
</form>
<form action={deleteBook.bind(null, book.id)}>
<button type="submit">Delete</button>
</form>
</article>
);
}deleteBook.bind(null, book.id) is a function that calls deleteBook(book.id, formData) when the form submits. The null is the value of this, which you don't need.
Notice there is no 'use client' here. BookCard is a Server Component, because a form with a Server Function action needs no React state. It even works before the page's JavaScript has loaded: the browser submits the form normally, the server runs the function, and sends back the new page. This is called progressive enhancement.
Here is the same pattern in the browser, with pretend server functions:
Why does BookCard use deleteBook.bind(null, book.id) instead of () => deleteBook(book.id)?
Rate a book
Day 4's reducer had a rated action. Bring it back as a Server Function. Add setRating(id, rating) to lib/books.ts, a rateBook Server Function to app/actions.ts, and a small form on the detail page with a <select name="rating"> holding 1 to 5. Validate the rating on the server, and refresh the detail page afterwards.
Show solution
export async function setRating(id: string, rating: number): Promise<void> {
books = books.map((book) => (book.id === id ? { ...book, rating } : book));
}export async function rateBook(id: string, formData: FormData) {
const rating = Number(formData.get('rating'));
if (!Number.isInteger(rating) || rating < 1 || rating > 5) {
return; // ignore anything that isn't a whole number from 1 to 5
}
await setRating(id, rating);
revalidatePath(`/books/${id}`);
}Remember to import setRating at the top of app/actions.ts.
import { rateBook } from '@/app/actions';
// ...inside the returned JSX, below the author:
<form action={rateBook.bind(null, book.id)}>
<label htmlFor="rating">Your rating</label>{' '}
<select id="rating" name="rating" defaultValue={book.rating ?? 3}>
{[1, 2, 3, 4, 5].map((n) => (
<option key={n} value={n}>
{n}
</option>
))}
</select>{' '}
<button type="submit">Save rating</button>
</form>The bound id arrives first and the form data second. Number() turns the text from the select into a number, and the check rejects anything a hand-made request might send, such as "99" or "abc". revalidatePath uses the literal path of this book's page.
Day 7 progress
Server and Client Components
Learn which components run on the server, which run in the browser, how 'use client' draws the line, and how to stream slow data with Suspense.
Data and caching
Learn how Next.js splits a page into a static shell and streamed parts, how to cache data with 'use cache', and how to turn on the React Compiler.