Skip to content

React Server Components

React is used to build your user interface. By default, all components are server components. That means that the component is rendered on the server as HTML and then streamed to the client. These do not include any client-side interactivity.

export default function MyServerComponent() {
return <div>Hello, from the server!</div>;
}

When a user needs to interact with your component: clicking a button, setting state, etc, then you must use a client component. Mark the client component with the "use client" directive. This will be hydrated by React in the browser.

"use client";
export default function MyClientComponent() {
return <button>Click me</button>;
}

React Server Components run on the server, they can easily fetch data and make it part of the payload that’s sent to the client.

src/app/pages/todos/TodoPage.tsx
export async function Todos({ ctx }) {
const todos = await db.todo.findMany({ where: { userId: ctx.user.id } });
return (
<ol>
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ol>
);
}
export async function TodoPage({ ctx }) {
return (
<div>
<h1>Todos</h1>
<Suspense fallback={<div>Loading...</div>}>
<Todos ctx={ctx} />
</Suspense>
</div>
);
}

The TodoPage component is a server component. It is rendered by a route, so it receives the ctx object. We pass this to the Todos component, which is also a server component, and renders the todos.

Allow you to execute code on the server from a client component.

@/pages/todos/functions.tsx
"use server";
import { requestInfo } from "rwsdk/worker";
export async function addTodo(formData: FormData) {
const { ctx } = requestInfo;
const title = formData.get("title");
await db.todo.create({ data: { title, userId: ctx.user.id } });
}

The addTodo function is a server function. It is executed on the server when the form is submitted from a client side component. The form data is sent to the server and the function is executed. The result is streamed back to the client, parsed by React, and the view is updated with the new todo.

@/pages/todos/AddTodo.tsx
"use client";
import { addTodo } from "./functions";
export default function AddTodo() {
return (
<form action={addTodo}>
<input type="text" name="title" />
<button type="submit">Add</button>
</form>
);
}

Standard React Server Action calls typically expect the server to return the entire updated UI tree so the client can rehydrate the page. For many interactions—especially queries where you only need the returned data—this is unnecessary overhead. To give you more control over this behavior, RedwoodSDK provides serverQuery and serverAction wrappers.

Use serverQuery for fetching data.

  • Method: GET (default).
  • Behavior: Returns data only. Does not rehydrate or re-render the page.
  • Location: Must be in a "use server" file. We recommend queries.ts.
queries.ts
"use server";
import { serverQuery } from "rwsdk/worker";
import { isAuthenticated } from "@/lib/auth"; // Hypothetical auth utility
// Simple query
export const getTodos = serverQuery(async (userId: string) => {
return db.todo.findMany({ where: { userId } });
});
// Query with middleware (e.g. auth check)
export const getSecretData = serverQuery([
async () => {
// Check auth
if (!isAuthenticated()) {
throw new Response("Unauthorized", { status: 401 });
}
},
async () => {
return "Secret Data";
}
]);

Use serverAction for mutations.

  • Method: POST (default).
  • Behavior: Rehydrates and re-renders the page with the updated server state.
  • Location: Must be in a "use server" file. We recommend actions.ts.
actions.ts
"use server";
import { serverAction } from "rwsdk/worker";
import { isAuthenticated } from "@/lib/auth"; // Hypothetical auth utility
export const createTodo = serverAction(async (title: string) => {
await db.todo.create({ data: { title } });
});
// Action with middleware
const requireAuth = async () => {
// Auth check
if (!isAuthenticated()) {
throw new Response("Unauthorized", { status: 401 });
}
}
export const deleteTodo = serverAction([
requireAuth,
async (id: string) => {
await db.todo.delete({ where: { id } });
}
]);
// You can also customize the HTTP method
export const searchTodos = serverAction(
async (query: string) => {
// ...
},
{ method: "GET" }
);

Behind the scenes, serverQuery uses a specialized optimization of the RSC protocol to enable fast, data-only fetches without the overhead of a full page re-render.

Standard React Server Action calls typically expect the server to return the entire updated UI tree so the client can rehydrate the page. For queries where you only need the returned data, this is unnecessary overhead.

When you call a function wrapped in serverQuery, the client sends a special x-rsc-data-only: true header.

The RedwoodSDK server recognizes this header and skips the expensive process of rendering your Page components. Instead, it returns a minimal RSC payload:

  1. node: null: Tells React there is no UI change required.
  2. actionResult: Contains the actual data returned by your function.

This allows RedwoodSDK to resolve the function call result directly in your client component while keeping the current UI state perfectly intact, avoiding any flickering or unnecessary hydration cycles.

Context is a way to share data globally between server components on a per-request basis. The context is populated by middleware, and is available to all React Server Components Pages and Server Functions via the ctx prop or requestInfo.ctx.

Server Functions can return standard Response objects, which is particularly useful for performing redirects or setting custom headers after an action completes.

When a Response is returned, RedwoodSDK automatically handles it:

  • Redirects: If the response has a 3xx status code and a Location header, the client will automatically redirect.
  • Custom Responses: Other response types are also supported, and their metadata (status, headers) is made available on the client.
src/pages/todos/functions.tsx
"use server";
export async function addTodo(formData: FormData) {
// ... logic to add todo ...
// Redirect to the todos list page after success
return Response.redirect("/todos", 303);
}

You can intercept action responses on the client by providing an onActionResponse callback to initClient. This is useful if you want to handle redirects manually or perform side effects based on the response.

src/entry.client.tsx
import { initClient } from "rwsdk/client";
initClient({
onActionResponse: (response) => {
console.log("Action returned status:", response.status);
// Return true to prevent the default redirect behavior
// return true;
},
});

RedwoodSDK also provides a way to render your React Server Components imperatively with renderToStream() and renderToString().

To render your component tree to a ReadableStream

renderToStream(element[, options]): Promise<ReadableStream> Experimental

Section titled “renderToStream(element[, options]): Promise<ReadableStream> ”

Takes in a React Server Component (can be a client component or server component), and returns a stream that decodes to html.

const stream = await renderToStream(<NotFound />, { Document })
const response = new Response(stream, {
status: 404,
});
  • Document: The document component to wrap around the React Server Component element. If not given, will return the rendered React Server Component without any wrapping.
  • injectRSCPayload = false: Whether to inject the corresponding RSC payload for the React Server Component to use for client-side hydration
  • onError: A callback function called with the relevant error as the only paramter if any errors happen during rendering

renderToString(element[, options]): Promise<string> Experimental

Section titled “renderToString(element[, options]): Promise<string> ”

Takes in a React Server Component (can be a client component or server component), and returns an html string.

const html = await renderToString(<NotFound />, { Document })
const response = new Response(html, {
status: 404,
});
  • Document: The document component to wrap around the React Server Component element. If not given, will return the rendered React Server Component without any wrapping.
  • injectRSCPayload = false: Whether to inject the corresponding RSC payload for the React Server Component to use for client-side hydration