Client components
When you need interactivity on the client side, you can use client components. Client components will be also rendered on the server side, but the client component will be hydrated on the client side.
All client components will be loaded asynchronously, so they will not block the rendering of the page. All client component will be compiled into an ES module and get lazy loaded only when needed.
To create a client component, add the "use client"; pragma at the top of the file.
"use client";
export default function MyClientComponent() {
return <p>This is a client component</p>;
}
Client components can use any React hooks, like the useState hook and can attach event handlers to elements like onClick. The client components are rendered both on the server side and on the client side.
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Client components can receive props from server components. The props will get serialized and passed to the client. All props passed to a client component must be serializable. You can pass any primitive values, arrays and objects, but functions are not allowed.
"use client";
export default function MyClientComponent({ name }) {
return <p>Hello {name}</p>;
}
Using the above client component from a server component:
import MyClientComponent from "./MyClientComponent";
export default function MyServerComponent() {
return <MyClientComponent name="John" />;
}
You can also wrap server components into client components. This is very useful when you want to use a server component inside a client component, like a React Context provider. The context will be available in all child client components. As the context is created only on the client, server components will not have access to the context.
"use client";
import { createContext } from "react";
const MyContext = createContext("unknown");
export default function MyProvider({ name, children }) {
return <MyContext.Provider value={name}>{children}</MyContext.Provider>;
}
import MyProvider from "./MyProvider";
export default async function MyServerComponent() {
const name = await getUserName();
return (
<MyProvider name={name}>
<p>Hello {name}</p>
</MyProvider>
);
}
Instead of creating a separate file for each client component, you can use the "use client" directive inside a function body. This allows you to define client components inline, right next to the server component that uses them.
import { useState } from "react";
function Counter() {
"use client";
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default function App() {
return (
<div>
<h1>My App</h1>
<Counter />
</div>
);
}
The Counter function above will be automatically extracted into a separate client component module. The server component App will render a client reference to Counter instead of the function itself.
You can also use inline client components inside other functions. The framework will automatically detect any variables captured from the parent scope and pass them as props to the extracted client component.
import { useState } from "react";
export default function App() {
const label = "clicks";
const Counter = () => {
"use client";
const [count, setCount] = useState(0);
return (
<div>
<p>{label}: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
return (
<div>
<h1>My App</h1>
<Counter />
</div>
);
}
In this example, the label variable is captured from App's scope and automatically forwarded as a prop to the client component. Module-level imports and top-level declarations used by the inline component are included in the extracted module directly.
Note: All captured variables must be serializable, just like regular client component props. Functions, class instances, and other non-serializable values cannot be captured.
You can define "use server" functions directly inside an inline "use client" component. This lets you keep the server logic and the client UI that calls it in a single file, without creating separate modules for either.
import { useState, useTransition } from "react";
export default function App() {
function Counter() {
"use client";
const [count, setCount] = useState(0);
const [, startTransition] = useTransition();
async function increment(n) {
"use server";
return n + 1;
}
return (
<div>
<p>Count: {count}</p>
<button
onClick={() =>
startTransition(async () => {
setCount(await increment(count));
})
}
>
Increment
</button>
</div>
);
}
return <Counter />;
}
The framework extracts each directive into its own module automatically. The Counter component becomes a client module, and increment becomes a server function — no extra files needed.
You can define inline "use client" components inside a file that already has a top-level "use server" directive. This lets a server function define and return interactive client components without needing a separate file.
"use server";
import { useState } from "react";
export async function createBadge(label) {
function Badge({ text }) {
"use client";
const [clicked, setClicked] = useState(false);
return (
<span onClick={() => setClicked(true)} style={{ cursor: "pointer" }}>
{clicked ? `clicked:${text}` : text}
</span>
);
}
return <Badge text={label} />;
}
The file is treated as a server function module because of the top-level "use server" directive, but the Badge component is extracted into a separate client module. When createBadge is called, it renders the Badge with the given props and returns it through the RSC protocol. The client component is fully interactive after hydration — useState, event handlers, and other client-side features all work as expected.
When the app's root module — the entry file you pass to react-server — is itself a "use client" module, the runtime detects this at startup and renders it through React DOM's renderToReadableStream directly, skipping the RSC flight pipeline entirely. There is nothing to enable; the rendering path is picked automatically based on the resolved root module's directive, and the choice applies to the whole app for the lifetime of the process.
src/index.ssr.jsx"use client";
import App from "./App.jsx";
export default function Root() {
return <App />;
}
src/index.rsc.jsximport App from "./App.jsx";
export default function Root() {
return <App />;
}
Both entries above produce the same HTML and the same hydrated React tree. The first goes through React DOM SSR directly. The second goes through the RSC pipeline, with App (a "use client" module) materialized as a client reference in the flight payload.
The children prop is the bridge that lets a server entry compose server components inside a client root:
src/index.rsc.jsximport App from "./App.jsx"; // "use client"
import Stats from "./Stats.jsx"; // server component
import Products from "./Products.jsx"; // server component
export default function Root() {
return (
<App>
<Stats />
<Products />
</App>
);
}
In this RSC entry, <Stats /> and <Products /> are evaluated as server components. The flight payload contains the pre-rendered React element trees, attached as App's children prop. No client-side JavaScript ships for them.
If the same JSX is written in a "use client" entry, the imports get pulled into the client bundle transitively, and <Stats /> and <Products /> become client components — they SSR through React DOM and hydrate normally:
src/index.ssr.jsx"use client";
import App from "./App.jsx";
import Stats from "./Stats.jsx";
import Products from "./Products.jsx";
export default function Root() {
return (
<App>
<Stats />
<Products />
</App>
);
}
The same component files are used in both entries. The directive on the entry decides their treatment.
"use cache: request" functions continue to work in client-root rendering. The runtime flushes resolved cache entries into the HTML stream as a self.__react_server_request_cache_entries__ payload, and the browser-side wrapper reads from it synchronously during hydration. A use(cachedFn()) call inside a client component resolves on the first try without suspending.
Server functions invoked through POST requests are the one exception to the shortcut: they are routed back through the standard RSC entry even when the app root is a client module, because server function dispatch needs the full RSC pipeline to emit a serverFunctionResult flight. This is invisible to authors — the server function runs and useActionState consumes the result exactly as it would for any RSC root.
Reach for client-root rendering when the whole app is going to be a client tree anyway: dashboard-style apps with pervasive interactivity, or single-page-application shells. For apps that are mostly server-rendered with a few interactive islands, the regular RSC pipeline remains the right default — you ship less JavaScript that way.
You can make a component client-only by wrapping the component into a ClientOnly component. Children of the ClientOnly component will be rendered only on the client side.
import { ClientOnly } from "@lazarv/react-server/client";
export default function MyServerComponent() {
return (
<div>
<p>This is rendered on the server side</p>
<ClientOnly>
<p>This is rendered on the client side</p>
<MyClientComponent />
</ClientOnly>
</div>
);
}
ClientOnly is a rendering guard — it controls when its children render, but it does not control what gets bundled. The wrapped component and its imports still ship in the SSR bundle and run during server rendering. For components that pull in heavy browser-only dependencies (WebGL, Canvas, IntersectionObserver-based libraries, anything that touches window at module scope), use the "use client; no-ssr" directive below — it removes the implementation from the SSR bundle entirely.
A "use client" component still renders on the server during SSR — only its interactivity is deferred to the client. Its module graph is part of the SSR bundle, so any imports it pulls in are bundled and evaluated on the server. For components whose dependencies only make sense in the browser — Three.js, charting libraries, code editors, anything that touches window or document at module scope — that means shipping (and parsing) hundreds of KiB of code into your edge worker just to render an empty wrapper.
The "use client; no-ssr" directive avoids this. It tells the runtime to compile the module differently for each environment:
- Server build: the module is replaced with a null-rendering stub. None of the original code or its imports appear in the SSR bundle.
- Client build: the module is wrapped in
ClientOnlyautomatically. The real component imports its dependencies as normal, but only renders after hydration — matching the server's null output and avoiding hydration mismatch.
src/components/Scene.jsx"use client; no-ssr";
import { useEffect, useRef } from "react";
import * as THREE from "three";
export default function Scene() {
const ref = useRef(null);
useEffect(() => {
const renderer = new THREE.WebGLRenderer();
ref.current.appendChild(renderer.domElement);
// ...
return () => renderer.dispose();
}, []);
return <div ref={ref} />;
}
Used from a server component, the import looks identical to any other client component:
import Scene from "./components/Scene.jsx";
export default function Page() {
return (
<main>
<h1>Welcome</h1>
<Scene />
</main>
);
}
The server renders <main><h1>Welcome</h1></main> — no <Scene /> markup, no Three.js evaluation, no Three.js code in the SSR bundle. After the page hydrates, the browser loads the Scene chunk, runs the effect, and mounts the canvas in place.
Reach for "use client; no-ssr" when:
- The component depends on browser-only globals (
window,document,navigator, WebGL/Canvas contexts) at module scope. - The dependency graph is large and only meaningful in the browser (3D scenes, rich-text editors, video players, charting libraries with DOM measurement).
- You want the module to be a separate client chunk that loads only on pages where the component appears.
For interactive components that have no browser-only dependencies, plain "use client" is the right default — it gives you SSR markup for free, which is what users see before hydration completes.
Note: Like
ClientOnly, a"use client; no-ssr"component renders nothing until after hydration. Reserve it for components where the alternative (no SSR markup) is acceptable, and consider rendering a placeholder around it for layout stability.