| name | react-server |
| description | Build applications with @lazarv/react-server — a React Server Components runtime built on Vite. Covers use directives, file-system router, HTTP hooks, caching, live components, workers, MCP, deployment, and all core APIs. |
You are working on a project that uses @lazarv/react-server — an open React Server Components runtime (not a framework). It is built on the Vite Environment API and supports Node.js, Bun, and Deno.
Full documentation: https://react-server.dev
Any docs page as markdown: https://react-server.dev/{path}.md
JSON Schema for configuration: https://react-server.dev/schema.json
Examples: https://github.com/lazarv/react-server/tree/main/examples
When you need more detail on a specific topic, fetch the relevant markdown page from the docs site.
CLI Commands
pnpm react-server ./App.jsx
pnpm react-server
pnpm react-server build [./App.jsx]
pnpm react-server start
Use Directives
These directives go at the top of a file or inside a function body (lexically scoped RSC):
"use client" — Client component (enables React hooks, event handlers, browser APIs)
"use server" — Server function (callable from client, receives FormData as first arg)
"use live" — Live component (async generator that yields JSX, streams updates via WebSocket)
"use worker" — Worker module (offloads to Worker Thread on server, Web Worker on client)
"use cache" — Cached function with options: "use cache; ttl=200; tags=todos" or "use cache; profile=todos" or "use cache: file; tags=todos" or "use cache: request" (per-request dedup) or "use cache: request; no-hydrate" (no browser hydration)
"use dynamic" — Force dynamic rendering (opt out of static/prerender)
"use static" — Force static rendering at build time
"use hydrate" — Hydration island: render a server subtree as HTML immediately, write a separate island RSC payload, and hydrate it later as a local non-root outlet. Syntax: "use hydrate: visible; rootMargin=0px; threshold=0.2; id=counter".
Hydration Islands
Use hydration islands when the page root should stay server-only but a selected subtree needs deferred interactivity. Put the directive inside a server component function body:
function CounterIsland() {
"use hydrate: visible; rootMargin=0px; threshold=0.2; id=counter";
return <Counter />;
}
Key behavior:
- The island is SSR-rendered immediately.
- The runtime emits request-scoped hydration data for the island and can omit the main
PAGE_ROOT RSC payload when no page-root hydration is needed.
- The client hydrates the island as a local outlet, not as a remote component.
- Hydration islands are created during initial HTML rendering; if a
"use hydrate" component appears in a later RSC update payload, it renders as normal React content because the parent tree already owns it.
- Components inside the island can use
Link local and Refresh local to navigate or refresh only the island outlet.
- DevTools shows islands in the Outlets panel with an
island badge and hydrated/not hydrated state.
Strategies:
load — hydrate as soon as the client entry runs and the island payload is available. This is the default for "use hydrate" and is best for above-the-fold controls that should become interactive immediately while still using a local outlet.
idle — hydrate through requestIdleCallback, falling back to setTimeout. Supports timeout in milliseconds; default is 2000.
visible — hydrate when an IntersectionObserver sees the island marker. Supports rootMargin (default 600px) and threshold (default 0). If IntersectionObserver is unavailable, hydrate immediately.
interaction — hydrate on user interaction. Default events are pointerenter, focusin, pointerdown, and click; override with events=pointerenter,focusin. The triggering event starts hydration and should not be treated as replayed into the hydrated component.
media — hydrate when matchMedia(query) matches. If the query already matches, hydrate immediately; otherwise listen for changes. Missing query or unavailable matchMedia falls back to immediate hydration. Use this for motion preferences too, e.g. query=(prefers-reduced-motion: no-preference) or query=(prefers-reduced-motion: reduce).
never — render static HTML without creating a hydration payload. Client component effects and handlers inside the island never run.
File-System Router Conventions
When no entrypoint is passed to the CLI, the file-system router is used. Default root is src/pages (configurable via root in config).
src/pages/
├── layout.jsx # Root layout (wraps all routes)
├── page.jsx # Index page (/)
├── about.jsx # /about (any .jsx/.tsx file is a page)
├── (group)/ # Route group (no URL segment)
│ └── page.jsx
├── users/
│ ├── page.jsx # /users
│ ├── [id].page.jsx # /users/:id (dynamic segment)
│ └── [id]/
│ └── posts.page.jsx # /users/:id/posts
├── docs/
│ └── [...slug].page.jsx # /docs/* (catch-all, slug is string[])
├── @sidebar/ # Parallel route / outlet named "sidebar"
│ └── page.jsx
├── loading.page.jsx # Loading fallback (Suspense boundary)
├── error.page.jsx # Error fallback (ErrorBoundary)
├── index.middleware.mjs # Middleware for this route segment
├── GET.posts.server.mjs # API route: GET /posts
└── mcp.server.mjs # MCP endpoint at /mcp
Key conventions:
page.jsx or index.jsx — route page (index route)
"use client" page — client-only route (no server round-trip, state preserved between navigations)
layout.jsx — wraps child routes with persistent layout
[param] — dynamic route parameter (passed as prop)
[...slug] — catch-all (param is string[])
[[...slug]] — optional catch-all
(name) — route group / transparent segment (not in URL)
@name/ — parallel route outlet
*.middleware.{js,mjs,ts,mts} — middleware
*.server.{js,mjs,ts,mts} — API route handler
GET.*.server.mjs / POST.*.server.mjs — HTTP method-specific API route
{filename.xml}.server.mjs — escaped route segment for special characters
*.resource.{js,mjs,ts,mts} — resource file (auto-bound to route)
Route validation (export validate object with params/search schemas from any page):
import { z } from "zod";
import { user } from "@lazarv/react-server/routes";
export const validate = {
params: z.object({ id: z.string().regex(/^\d+$/) }),
};
export default user.createPage(({ id }) => <h1>User {id}</h1>);
Auto-generated routes module (@lazarv/react-server/routes): exports named route descriptors for every route. Each has .Link, .href(), .useParams(), .useSearchParams(), .createPage(), .createLayout(), .createLoading(), .createError(), .createMiddleware().
Imports Quick Reference
import {
useHttpContext,
useUrl,
usePathname,
useSearchParams,
useRequest,
useResponse,
useFormData,
headers,
cookie,
setCookie,
deleteCookie,
status,
redirect,
rewrite,
after,
useCache,
useResponseCache,
withCache,
revalidate,
invalidate,
getRuntime,
} from "@lazarv/react-server";
import { defineConfig } from "@lazarv/react-server/config";
import { Link, Refresh, ReactServerComponent, ScrollRestoration, useNavigate } from "@lazarv/react-server/navigation";
import { useClient } from "@lazarv/react-server/navigation";
import { createRoute, createRouter, Route, SearchParams } from "@lazarv/react-server/router";
import { createResource, createResources, resources } from "@lazarv/react-server/resources";
import { index, about, user } from "@lazarv/react-server/routes";
import { ErrorBoundary } from "@lazarv/react-server/error-boundary";
import { ClientOnly } from "@lazarv/react-server/client";
import RemoteComponent from "@lazarv/react-server/remote";
import { isWorker } from "@lazarv/react-server/worker";
import { createServer, createTool, createResource as createMcpResource, createPrompt } from "@lazarv/react-server/mcp";
Typed Router
@lazarv/react-server includes a fully typed routing solution with compile-time type safety for route paths, params, and search params. No code generation step — works with any schema library (Zod, ArkType, Valibot) or lightweight parse functions.
Defining routes with createRoute
import { createRoute } from "@lazarv/react-server/router";
import { z } from "zod";
export const home = createRoute("/", { exact: true });
export const user = createRoute("/user/[id]", {
exact: true,
validate: { params: z.object({ id: z.coerce.number().int().positive() }) },
});
export const products = createRoute("/products", {
exact: true,
validate: {
search: z.object({
sort: z.enum(["name", "price", "rating"]).catch("name"),
page: z.coerce.number().int().positive().catch(1),
}),
},
});
export const post = createRoute("/post/[slug]", {
exact: true,
parse: { params: { slug: String }, search: { tab: String } },
});
export const notFound = createRoute("*");
export const userNotFound = createRoute("/user/*");
Composing routes with createRouter (server)
import { createRoute, createRouter } from "@lazarv/react-server/router";
import * as routes from "./routes";
const router = createRouter({
home: createRoute(routes.home, <Home />),
user: createRoute(routes.user, <UserPage />),
notFound: createRoute(routes.notFound, <NotFound />),
});
export default function App() {
return (
<div>
<nav>
<router.home.Link>Home</router.home.Link>
<router.user.Link params={{ id: 42 }}>User 42</router.user.Link>
</nav>
<router.Routes />
</div>
);
}
Client-only routes
Routes without server components — useful for tabs, modals, filters. Define with createRoute (no element) and use in "use client" components:
"use client";
import { settings } from "./routes";
export default function SettingsPage() {
const params = settings.useParams();
return (
<nav>
<settings.Link params={{ tab: "profile" }}>Profile</settings.Link>
<settings.Link params={{ tab: "security" }}>Security</settings.Link>
</nav>
);
}
In the file-system router, any page with "use client" at the top is automatically a client-only route.
Typed hooks and Link
Every route descriptor provides: .Link (typed Link component), .href(params?) (URL builder), .useParams() (typed params or null), .useSearchParams() (typed search params), .SearchParams (route-scoped transform boundary).
Functional search param updaters
<products.Link search={(prev) => ({ ...prev, page: prev.page + 1 })}>
Next Page
</products.Link>
SearchParams transform boundary
Bidirectional URL ↔ app search param transforms with decode and encode:
<products.SearchParams decode={decode} encode={encode}>
{children}
</products.SearchParams>
Programmatic navigation
import { useNavigate } from "@lazarv/react-server/navigation";
const navigate = useNavigate();
navigate(user, { params: { id: 42 } });
Resources (Typed Data Fetching)
Schema-validated, route-aware data fetching with "use cache" as the caching runtime.
import { createResource } from "@lazarv/react-server/resources";
import { z } from "zod";
export const userById = createResource({
key: z.object({ id: z.coerce.number().int().positive() }),
}).bind(async ({ id }) => {
"use cache";
return db.users.findById(id);
});
export const currentUser = createResource().bind(async () => {
return (await getSession()).user;
});
Using in components:
const userData = userById.use({ id });
const user = await userById.query({ id: 42 });
userById.prefetch({ id: 42 });
Route-resource bindings for automatic pre-loading:
const todosMapping = todos.from(({ search }) => ({ filter: search.filter ?? "all" }));
Scroll Restoration
Enable via config or component:
export default { scrollRestoration: true };
export default { scrollRestoration: { behavior: "smooth" } };
import { ScrollRestoration } from "@lazarv/react-server/navigation";
<ScrollRestoration behavior="smooth" />
Handles: forward navigation (scroll to top or #hash), back/forward (restore saved position), page refresh (zero-flash restore), query-only changes (preserve position), nested scroll containers, prefers-reduced-motion support.
Component Patterns
Server Component (default — no directive needed)
export default async function Page() {
const data = await db.query("SELECT * FROM posts");
return <ul>{data.map(post => <li key={post.id}>{post.title}</li>)}</ul>;
}
Client Component
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
}
Server Function
"use server";
export async function createPost(formData) {
const title = formData.get("title");
await db.insert({ title });
}
export default function Page() {
async function handleSubmit(formData) {
"use server";
await db.insert({ title: formData.get("title") });
}
return (
<form action={handleSubmit}>
<input name="title" />
<button type="submit">Create</button>
</form>
);
}
Live Component (real-time streaming)
"use live";
export default async function* LiveClock() {
while (true) {
yield <div>{new Date().toLocaleTimeString()}</div>;
await new Promise(r => setTimeout(r, 1000));
}
}
Worker Function
"use worker";
export async function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
Lexically Scoped RSC (inline directives)
export default async function Page() {
const data = await fetchData();
function InteractiveList({ items }) {
"use client";
const [filter, setFilter] = useState("");
return (
<>
<input value={filter} onChange={e => setFilter(e.target.value)} />
<ul>{items.filter(i => i.includes(filter)).map(i => <li key={i}>{i}</li>)}</ul>
</>
);
}
return <InteractiveList items={data} />;
}
Server → Client → Server nesting is also supported:
async function getData() {
"use server";
function Display({ value }) {
"use client";
const [show, setShow] = useState(false);
return <button onClick={() => setShow(!show)}>{show ? value : "Reveal"}</button>;
}
const value = await db.getSecret();
return <Display value={value} />;
}
HTTP Hooks
Available in server components, middlewares, and API routes:
import {
useUrl,
usePathname,
useSearchParams,
useRequest,
useResponse,
useFormData,
useHttpContext,
headers,
cookie,
setCookie,
deleteCookie,
status,
redirect,
rewrite,
after,
} from "@lazarv/react-server";
Caching
import { useResponseCache, useCache, withCache, revalidate, invalidate } from "@lazarv/react-server";
useResponseCache(30_000);
export default withCache(async function Page() { ... }, 30_000);
const data = await useCache(
["posts", category],
() => db.query("SELECT * FROM posts WHERE category = ?", [category]),
60_000
);
revalidate(["posts", category]);
Using the cache directive:
"use cache; ttl=30000; tags=posts";
export async function getPosts() {
return db.query("SELECT * FROM posts");
}
import { invalidate } from "@lazarv/react-server";
await invalidate(getPosts);
Built-in cache providers
memory — default in-memory cache
request — per-request deduplication, shared across RSC and SSR (see below)
null — no-op cache (useful with aliases to disable caching)
local — browser localStorage
session — browser sessionStorage
Request-scoped caching
The request provider deduplicates calls within a single HTTP request. The function body runs once — all subsequent calls (including across RSC and SSR environments) return the cached result. Values are automatically dehydrated into the HTML and rehydrated in the browser during React hydration.
async function getPageData() {
"use cache: request";
return await fetchExpensiveData();
}
async function ServerPart() {
const data = await getPageData();
return <div>{data.title}</div>;
}
"use client";
import { use } from "react";
function ClientPart() {
const data = use(getPageData());
return <div>{data.title}</div>;
}
Disable hydration (don't embed value in HTML) with either syntax:
"use cache: request; hydrate=false";
"use cache: request; no-hydrate";
When hydrate=false / no-hydrate is set, SSR deduplication still works but the value is not embedded in the HTML — the client component recomputes it in the browser.
Configuration
import { defineConfig } from "@lazarv/react-server/config";
export default defineConfig({
root: "src/pages",
public: "public",
port: 3000,
adapter: "vercel",
scrollRestoration: true,
mdx: {
remarkPlugins: [],
rehypePlugins: [],
components: "./src/mdx-components.jsx",
},
cache: {
profiles: {
short: { ttl: 60_000 },
},
},
telemetry: {
enabled: true,
serviceName: "my-app",
},
});
JSON config with schema validation:
{
"$schema": "https://react-server.dev/schema.json",
"port": 3000,
"adapter": "vercel"
}
Extension configs are merged: +tailwind.config.mjs, +auth.config.mjs, etc.
Mode-specific: .production.config.mjs, .development.config.mjs, .build.config.mjs.
Env variables: VITE_* and REACT_SERVER_* prefixed vars are exposed via import.meta.env.
Navigation
import { Link, Refresh, ReactServerComponent, ScrollRestoration, useNavigate } from "@lazarv/react-server/navigation";
<Link href="/about" prefetch>About</Link>
<user.Link params={{ id: 42 }}>User 42</user.Link>
<products.Link search={(prev) => ({ ...prev, page: prev.page + 1 })}>Next</products.Link>
<Refresh>Reload</Refresh>
<ReactServerComponent outlet="sidebar">{sidebar}</ReactServerComponent>
<ScrollRestoration behavior="smooth" />
import { useClient } from "@lazarv/react-server/navigation";
const { navigate, replace, prefetch } = useClient();
const nav = useNavigate();
nav(user, { params: { id: 42 } });
Error Handling
import { ErrorBoundary } from "@lazarv/react-server/error-boundary";
<ErrorBoundary
fallback={<p>Loading...</p>}
component={({ error, resetErrorBoundary }) => (
<div>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
)}
>
<Content />
</ErrorBoundary>
File router conventions: error.page.jsx for error boundaries, loading.page.jsx for Suspense fallbacks.
Middleware
import { redirect, rewrite, usePathname, useMatch } from "@lazarv/react-server";
export default function Middleware() {
if (useMatch("/old-path")) {
redirect("/new-path");
}
}
API Routes
import { useSearchParams } from "@lazarv/react-server";
export default function GetPosts() {
const params = useSearchParams();
return Response.json({ posts: [], page: params.get("page") });
}
Deployment Adapters
Available adapters: vercel, netlify, cloudflare, aws, azure, azure-swa, bun, deno, docker, firebase, singlefile.
export default { adapter: "vercel" };
export default { adapter: ["cloudflare", { serverlessFunctions: false }] };
Micro-Frontends
import RemoteComponent from "@lazarv/react-server/remote";
export default function App() {
return (
<RemoteComponent
src="http://localhost:3001"
defer // streaming
isolate // Shadow DOM isolation
message="Hello"
/>
);
}
MCP (Model Context Protocol)
import { createServer, createTool } from "@lazarv/react-server/mcp";
import { z } from "zod";
export default createServer({
tools: {
search: createTool({
id: "search",
title: "Search",
description: "Search the database",
inputSchema: { query: z.string() },
async handler({ query }) {
return await db.search(query);
},
}),
},
});