[Ctrl J]
[Ctrl K]
light/dark
Let's start by creating a simple button component in Next.js within the app directory:
const sayHello = () => {console.log("hello");};const Button = () => {return <button onClick={sayHello}>Click me</button>;};
At first glance, this code appears correct and should function as expected. However, when implemented in a Next.js application within the app directory, it doesn't behave as anticipated. This seeming inconsistency stems from a fundamental feature of Next.js.
The root cause lies in how Next.js handles components within the app directory. By default, all components created here are React Server Components. These differ significantly from the traditional React components we're accustomed to. This default behavior explains why our button component, which relies on client-side interactivity, fails to function as expected.
The Solution: "use client" Directive
"use client";// component code here
This directive tells Next.js to process this file and its dependencies as Client Components, enabling client-side interactivity and restoring the expected behavior of our button.
Understanding this distinction between Server and Client Components is crucial when working with Next.js, especially within the app directory. It allows developers to leverage the benefits of server-side rendering where appropriate while maintaining the flexibility to use client-side features when necessary.
Why Use React Server Components?
React Server Components (RSCs) offer advantages over traditional Client Components. By rendering parts of the application on the server, RSCs can reduce the amount of JavaScript sent to the client, improving performance. Additionally, server-side rendering enhances Search Engine Optimization (SEO) by providing search engines with pre-rendered HTML content. Furthermore, RSCs allow for direct access to backend resources without the need for extra API routes, streamlining data fetching and reducing complexity. Let's explore how to leverage RSCs effectively.
Transforming Client-Side Logic to Server-Side
Consider this server side component:
// ...const sayHello = () => {console.log("hello");};const Button = () => {return <button onClick={sayHello}>Click me</button>;};// ...
The sayHello
function won't work in an RSC because it relies on client-side event handling. Let's adapt it for server-side execution. To make our sayHello
function work within an RSC, we'll convert it to a server action:
const sayHello = async () => {"use server";console.log("hello");};
The "use server"
directive marks this function as a server action, allowing it to run on the server.
We can't use onClick
in an RSC, but we can use server actions with forms:
const sayHello = async () => {"use server";console.log("hello");};const Button = () => {return (<form action={sayHello}><button type="submit">Click me</button></form>);};
This works because React extends the HTML <form>
element to support server actions via the action
prop.
Now, run this code and click the button. Look at the browser – you won't see anything happen. But wait, check the terminal's console – you'll see "hello" logged there!
sayHello
function asynchronous and adding the "use server"
directive, we turn it into a server action
.action
prop in a form.server actions
run on the server NOT the browser.An Expensive Use Case
We can leverage the fact that server actions run on the server to run expensive calculation. Let's modify our sayHello
function to simulate calculating the factorial of a very large number.
const factorial = async () => {"use server";// let's pretend it takes 3 seconds to calculate the factorial on nawait new Promise((resolve) => setTimeout(resolve, 3000));console.log("The factorial of n is ★★★");};
Since we're working with a form, let's modify our code to submit data to the server action.
const sayHello = async (formData: FormData) => {"use server";const number = formData.get("number");// let's pretend it takes 3 seconds to calculate the factorial of nawait new Promise((resolve) => setTimeout(resolve, 3000));const message = `The factorial of ${number} is ★★★`;console.log(message);};const Button = () => {return (<form action={sayHello}>{/* we don't need to keep track of the state of this input */}<input type="text" name="number" /><button type="submit">Click me</button></form>);};
If you run this code, click the button, and then patiently check the terminal's console after 3 seconds. You should see something logged there.
Perfect! Let's summarize what we've done so far:
Server actions
, which run exclusively on the server, allow us to offload expensive calculations.formData
methods within the server action. This eliminates the need to manage form state with useState
.RSCs on their own aren't exactly magical or revolutionary, but when used together with server actions, they offer complementary advantages for building web applications. While we've focused on offloading expensive computations to the server, RSCs and server actions also excel at handling secure data and data validation.