Skip to content

Commit c8e529f

Browse files
authored
feat: hydration islands (#429)
## Summary This PR adds first-class hydration islands to `@lazarv/react-server` through a new inline `"use hydrate"` directive. A component marked with this directive is rendered as normal server HTML during the initial response, but its interactive React tree is isolated into island-scoped hydration data and hydrated later as a local non-root outlet. This allows mostly static pages to keep the page root unhydrated while selected subtrees become interactive according to an explicit strategy. ## Implementation The directive transform extracts `"use hydrate"` components in the same style as the existing inline directive pipeline and rewrites them to an internal hydration island component. On the server, the island renderer produces the initial HTML and a separate RSC payload for the island. In rootless pages, the client entry detects island markers and hydrates each island with `hydrateRoot`. On pages that already have a `PAGE_ROOT`, the island is represented by a client boundary inside the existing React tree; that boundary reads request-scoped hydration data, waits for the selected strategy when needed, and then renders a `ReactServerComponent` for the island outlet without creating another root. The island runtime supports `load`, `idle`, `visible`, `interaction`, `media`, and `never` strategies. Deferred strategies can delay both hydration and client module loading, and the manifest collection path now skips initial modulepreload entries for client modules that are only imported by deferred hydration island modules. When a `"use hydrate"` component appears later inside an RSC navigation or update payload, it intentionally renders as plain component output instead of creating a new island, because that subtree is already owned by the existing React tree. Hydration islands are registered as local outlets, so hydrated islands can use local `Link` navigation and `Refresh` without navigating or hydrating the page root. DevTools now identifies these outlets as islands rather than remotes and exposes their hydration state so pending, scheduled, hydrating, and hydrated islands are visible in the outlets panel. ## Documentation and Example The docs add a new Hydration Islands feature page that explains how islands differ from client components and PPR, documents each hydration strategy, and describes local outlet navigation, browser history behavior, and DevTools support. The comparison page was updated to reflect hydration islands, HTTP/runtime capabilities, observability, server function defenses, and server function middleware tradeoffs across React Server, Next.js, TanStack Start, React Router, Waku, and Astro where relevant. The LLM reference and React Server skill were also updated so agents know about the new directive and rendering model. A new `examples/hydration-islands` app demonstrates rootless hydration islands, mixed `PAGE_ROOT` mode, every supported strategy, visible client-component loading, RSC refresh, and local outlet navigation through `Link`.
1 parent 9cd6bb6 commit c8e529f

41 files changed

Lines changed: 3570 additions & 114 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/public/llms.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ No config files, no boilerplate, no separate React installation needed. Or use t
3232

3333
- **React Server Components**: The default rendering model. Components render on the server, support async/await, and their source code never reaches the client.
3434
- **Client Components**: Add `"use client"` directive to enable interactivity. Components are server-rendered then hydrated on the client.
35+
- **Hydration Islands**: Add `"use hydrate"` inside a server component function to render that subtree as HTML immediately and hydrate it later as a local non-root outlet. Strategies are `load`, `idle`, `visible`, `interaction`, `media`, and `never`. If rendered in a later RSC update payload, it returns normal React content instead of creating a new island.
3536
- **Server Functions**: Add `"use server"` to turn async functions into server-side RPC endpoints. Works as form actions with progressive enhancement.
3637
- **Typed Router**: Fully typed routing with `createRoute` and `createRouter`. Compile-time type safety for route paths, params, and search params. Schema validation with Zod, ArkType, Valibot, or lightweight parse functions. Typed `Link` components, typed hooks, typed programmatic navigation.
3738
- **File-System Router**: Activates automatically when no entrypoint is specified. Supports pages, layouts, outlets, API routes, middlewares, error boundaries, loading states, client-only routes, route validation, and auto-generated typed route descriptors via `@lazarv/react-server/routes`.
@@ -66,6 +67,7 @@ No config files, no boilerplate, no separate React installation needed. Or use t
6667
- [Middleware Mode](https://react-server.dev/features/middleware-mode): Running @lazarv/react-server as middleware inside existing servers like Express or NestJS in both dev and production
6768
- [Cluster Mode](https://react-server.dev/features/cluster): Running the production server in multi-process cluster mode to utilize all CPU cores
6869
- [Partial Pre-Rendering](https://react-server.dev/features/ppr): Pre-rendering static shells at build time while deferring dynamic content to runtime using "use dynamic" and "use static" directives
70+
- [Hydration Islands](https://react-server.dev/features/hydration-islands): Server-rendered subtrees marked with "use hydrate" that hydrate later as local non-root outlets without requiring PAGE_ROOT hydration; supports load, idle, visible, interaction, media, and never strategies; later RSC update payloads render them as normal React content
6971
- [Live Components](https://react-server.dev/features/live-components): Real-time streaming components using the "use live" directive with async generator functions pushing updates over WebSocket
7072
- [Workers](https://react-server.dev/features/worker): Offloading heavy computations to separate threads using "use worker" with RSC-based serialization — Node.js Worker Threads on server, Web Workers on client
7173
- [Micro-Frontends](https://react-server.dev/features/micro-frontends): Implementing micro-frontend architecture with RemoteComponent for loading and rendering remote React applications with SSR

docs/src/pages/en/(pages)/features/comparison.mdx

Lines changed: 99 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,14 @@ This comparison summarizes the feature set described in the documentation. It fo
2727

2828
| | @lazarv/react-server | Next.js | TanStack Start | React Router | Waku |
2929
|--|:---:|:---:|:---:|:---:|:---:|
30-
| React Server Components ||| 🛑 | 🟡 ||
30+
| React Server Components ||| 🟡 | 🟡 ||
3131
| Server Functions (actions) |||| 🟡 ||
32+
| Server Function Defense Layer | ✅ <br/> *encrypted refs + hardened decoder* | 🟡 <br/> *IDs/origin/body cap* | 🟡 <br/> *CSRF + validators* | 🔶 <br/> *route action model* | 🔶 <br/> *auth/userland* |
3233
| SSR ||||||
3334
| Streaming SSR ||||||
3435
| API Routes ||||||
3536
| API / Route Middleware ||||| 🟡 |
36-
| Server Function Middleware | 🟡 | 🛑 || 🛑 | 🛑 |
37+
| Server Function Middleware | 🔶 <br/> *not first-class* | 🛑 |<br/> *client/server chain* | 🔶 <br/> *not first-class* | 🛑 |
3738
| Static Site Generation ||| 🟡 | 🟡 ||
3839
| Partial Pre-rendering (PPR) ||| 🛑 | 🛑 | 🛑 |
3940
| Response Caching (TTL) ||| 🛑 | 🛑 | 🛑 |
@@ -49,6 +50,9 @@ This comparison summarizes the feature set described in the documentation. It fo
4950
|--|:---:|:---:|:---:|:---:|:---:|
5051
| Open Runtime (no vendor lock-in) || 🟡 <br/> *optimized for Vercel* ||||
5152
| Vite-based || 🛑 ||||
53+
| Production HTTP Layer | ✅ <br/> *backpressure + security* | 🟡 <br/> *platform-managed* | 🔶 <br/> *host/adapter* | 🔶 <br/> *host/adapter* | 🔶 <br/> *host/adapter* |
54+
| Observability | ✅ <br/> *OTel traces + metrics* | ✅ <br/> *OTel spans* | 🟡 <br/> *manual/experimental* | 🟡 <br/> *instrumentation API* | 🔶 <br/> *userland* |
55+
| Hydration Islands || 🛑 | 🟡 <br/> *deferred tree only* | 🛑 | 🛑 |
5256
| Multiple Deploy Targets || 🟡 ||||
5357
| Micro-frontend / Remote Components || 🔶 | 🛑 | 🛑 | 🛑 |
5458
| MCP Server Integration || 🛑 | 🛑 | 🛑 | 🛑 |
@@ -158,23 +162,106 @@ This comparison summarizes the feature set described in the documentation. It fo
158162
| Virtual Routes Module || 🛑 | 🟡 | 🟡 | 🛑 |
159163
| Route-scoped Loading / Error / Fallback Files ||| 🛑 | 🛑 | 🛑 |
160164
| Devtools || 🛑 | ✅ <br/> *router only* | 🟡 | 🛑 |
161-
| Route Masking | 🛑 | 🛑 || 🛑 | 🛑 |
165+
| Route Masking | 🔶 <br/> *URL identity model* | 🛑 || 🛑 | 🛑 |
162166
| Route-level Typed Dependencies | ✅ <br/> *Typesafe resources* | 🛑 | ✅ <br/> *Typesafe route context + loaders* | 🛑 | 🛑 |
163-
| Route Mount / Unmount Events | 🛑 | 🛑 || 🛑 | 🛑 |
167+
| Route Lifecycle Hooks | 🔶 <br/> *guards + React lifecycle* | 🛑 || 🛑 | 🛑 |
164168

165169
<Link name="notes">
166170
## Notes
167171
</Link>
168172

169173
`@lazarv/react-server` is a full React Server Components runtime — not just a router. The routing system is deeply integrated with RSC streaming, server functions, and the Vite build pipeline. This means features like typed routes, client-only routes, and server-side validation work end-to-end without glue code.
170174

175+
<Link name="key-architectural-differences">
171176
### Key architectural differences
177+
</Link>
178+
179+
<Link name="rsc-native-runtime">
180+
#### RSC-native runtime
181+
</Link>
182+
183+
Unlike routers that bolt RSC support on top, `@lazarv/react-server` was built from the ground up for React Server Components. Every route can mix server and client components freely.
184+
185+
<Link name="production-http-runtime">
186+
#### Production HTTP runtime
187+
</Link>
188+
189+
`@lazarv/react-server` ships a production runtime with keep-alive and timeout tuning, admission control, adaptive ELU-based backpressure, request/body and multipart limits, CSRF origin validation for action POSTs, health/readiness endpoints, and graceful shutdown. In many other React stacks, these concerns are delegated to the hosting platform, custom server, reverse proxy, or adapter rather than exposed as a first-class runtime layer.
190+
191+
<Link name="server-function-defense-layer">
192+
#### Server function defense layer
193+
</Link>
194+
195+
`@lazarv/react-server` treats server function invocation as hostile input before application code runs. Server function references are encrypted with AES-256-GCM capability tokens, support key rotation, and are decoded through configurable resource ceilings (`maxRows`, `maxDepth`, `maxBytes`, string/BigInt/stream limits) plus structural defenses against prototype pollution, forbidden path walks, hostile thenables, and forged callables. Next.js has meaningful Server Action defenses such as secure IDs, origin checks, closure encryption, and a body-size cap; TanStack Start has CSRF middleware and validators. The distinctive `react-server` behavior is the combined capability-token and hardened RSC reply-decoder layer as a first-class runtime boundary.
196+
197+
<Link name="server-function-middleware-model">
198+
#### Server function middleware model
199+
</Link>
200+
201+
TanStack Start has the most direct server-function middleware model in this comparison: function middleware can wrap server functions with client-side logic, server-side logic, input validation, composition, and global registration. `@lazarv/react-server` does not currently expose a first-class TanStack-style per-function middleware chain. Its request middleware runs before server function dispatch and can short-circuit or annotate the request, while `createFunction` provides protocol-level per-argument parse/validate contracts before the handler runs. Cross-cutting per-function policy can still be composed with userland wrappers, but the first-class runtime primitive is the request middleware plus contract/decoder boundary, not a client/server function middleware pipeline. React Router middleware wraps route `action`/`loader` requests, which is useful but tied to the route action model rather than arbitrary RPC server functions, so it is also not first-class support for this specific row.
202+
203+
<Link name="observability-runtime">
204+
#### Observability runtime
205+
</Link>
206+
207+
`@lazarv/react-server` has built-in OpenTelemetry integration that can automatically instrument HTTP requests, middleware, RSC/SSR rendering, server functions, cache lookups, server startup, and Vite dev hooks, plus built-in request, render, server function, and cache metrics. Telemetry dependencies are optional and lazy-loaded, so disabled telemetry resolves to no-op instrumentation. Next.js also has official OpenTelemetry instrumentation and framework spans. React Router exposes first-class instrumentation wrappers that apps wire into logging or tracing. TanStack Start documents observability patterns and manual/experimental OpenTelemetry setup, while Waku leaves this mostly to userland or the host platform.
208+
209+
<Link name="client-only-routes">
210+
#### Client-only routes
211+
</Link>
212+
213+
Pages with `"use client"` in the file-system router are automatically client-only — navigation skips the server entirely. No configuration needed, and component state is preserved across navigations via React's `<Activity>` component.
214+
215+
<Link name="schema-agnostic-validation">
216+
#### Schema-agnostic validation
217+
</Link>
218+
219+
Route params and search params can be validated with any schema library (Zod, ArkType, Valibot) or lightweight parse functions — the runtime detects the validation strategy automatically.
220+
221+
<Link name="rsc-typed-resources">
222+
#### RSC + typed resources
223+
</Link>
224+
225+
`@lazarv/react-server` offers two complementary data-fetching approaches: React Server Components with `async/await` (RSC as loaders), and **typed resources** — schema-validated, reference-identified data descriptors with `.use()` (suspense), `.query()` (imperative), `.prefetch()`, and `.invalidate()`. Resources use `"use cache"` as the caching runtime — no custom cache layer, no SWR boilerplate. Route-resource bindings enable parallel prefetching on navigation.
226+
227+
<Link name="route-level-dependencies">
228+
#### Route-level dependencies
229+
</Link>
230+
231+
TanStack Router exposes route context as a first-class typed primitive for passing dependencies (auth, DB clients, etc.) down the route tree. `@lazarv/react-server` models the same problem space through typed resources, request context, and native modules — there is no separate route-context bag because resources already carry schema-validated, route-scoped data with full type safety.
232+
233+
<Link name="built-in-devtools">
234+
#### Built-in devtools
235+
</Link>
236+
237+
`@lazarv/react-server` ships with integrated devtools (`--devtools`) that go beyond route inspection — they cover RSC payload, cache entries, routes, outlets, remote components, live components, workers, and server logs in a single panel. TanStack Router provides excellent router-focused devtools, but `@lazarv/react-server`'s devtools cover the full server component runtime.
238+
239+
<Link name="outlets-vs-parallel-routes">
240+
#### Outlets vs. parallel routes
241+
</Link>
242+
243+
`@lazarv/react-server` uses named outlets (`@sidebar`, `@content`) rendered as typed props to layouts. This is functionally similar to Next.js parallel routes but with stronger typing — each outlet is a branded React element that prevents accidentally swapping outlets.
244+
245+
<Link name="route-identity-over-masking">
246+
#### Route identity over route masking
247+
</Link>
248+
249+
`@lazarv/react-server` intentionally does not support TanStack-style route masking. The visible browser URL, server request URL, matched route tree, RSC payload, route validation, resources, cache keys, outlets, and devtools state are expected to describe the same navigation state. Use rewrites, redirects, route groups, outlets, or search params when a URL needs a different presentation, without creating hidden client-only route identity.
250+
251+
<Link name="route-lifecycle-model">
252+
#### Route lifecycle model
253+
</Link>
254+
255+
`@lazarv/react-server` exposes navigation guards (`useNavigationGuard`, `registerNavigationGuard`) for client-side page-root navigation decisions, including back/forward and optional `beforeUnload` handling. Internally, route guard/registration components manage route registration, active visibility, route resources, loading skeletons, and `Activity`-based preservation. User effects should live in React effects, resources, middleware, server functions, or cache invalidation rather than router-object mount/unmount callbacks.
256+
257+
<Link name="hydration-islands-vs-deferred-hydration">
258+
#### Hydration islands vs. deferred hydration
259+
</Link>
260+
261+
`@lazarv/react-server` hydration islands are initial-rendering boundaries that render server HTML immediately, store request-scoped island hydration data, and later hydrate as independent local outlets. This can work without a hydrated `PAGE_ROOT`. TanStack Start has deferred hydration-style behavior for suspended React subtrees, but that subtree still lives inside the page's React tree rather than becoming a separate island outlet with its own RSC payload. Astro is the closest architectural match outside this React/RSC comparison: it has a real island architecture for component-level hydration, but it is not an RSC local-outlet model. Other React frameworks can lazy-load client components or defer JavaScript in userland, but they do not provide this local-outlet island model as a first-class feature.
262+
263+
<Link name="waku-minimal-rsc">
264+
#### Waku
265+
</Link>
172266

173-
- **RSC-native**: Unlike routers that bolt RSC support on top, `@lazarv/react-server` was built from the ground up for React Server Components. Every route can mix server and client components freely.
174-
- **Client-only routes**: Pages with `"use client"` in the file-system router are automatically client-only — navigation skips the server entirely. No configuration needed, and component state is preserved across navigations via React's `<Activity>` component.
175-
- **Schema-agnostic validation**: Route params and search params can be validated with any schema library (Zod, ArkType, Valibot) or lightweight parse functions — the runtime detects the validation strategy automatically.
176-
- **RSC + typed resources**: `@lazarv/react-server` offers two complementary data-fetching approaches: React Server Components with `async/await` (RSC as loaders), and **typed resources** — schema-validated, reference-identified data descriptors with `.use()` (suspense), `.query()` (imperative), `.prefetch()`, and `.invalidate()`. Resources use `"use cache"` as the caching runtime — no custom cache layer, no SWR boilerplate. Route-resource bindings enable parallel prefetching on navigation.
177-
- **Route-level dependencies**: TanStack Router exposes route context as a first-class typed primitive for passing dependencies (auth, DB clients, etc.) down the route tree. `@lazarv/react-server` models the same problem space through typed resources, request context, and native modules — there is no separate route-context bag because resources already carry schema-validated, route-scoped data with full type safety.
178-
- **Built-in devtools**: `@lazarv/react-server` ships with integrated devtools (`--devtools`) that go beyond route inspection — they cover RSC payload, cache entries, routes, outlets, remote components, live components, workers, and server logs in a single panel. TanStack Router provides excellent router-focused devtools, but `@lazarv/react-server`'s devtools cover the full server component runtime.
179-
- **Outlets vs. parallel routes**: `@lazarv/react-server` uses named outlets (`@sidebar`, `@content`) rendered as typed props to layouts. This is functionally similar to Next.js parallel routes but with stronger typing — each outlet is a branded React element that prevents accidentally swapping outlets.
180-
- **Waku**: Waku is another RSC-native framework built on Vite, but takes a deliberately minimal approach. It provides basic file-based routing with layouts and RSC support, but lacks the typed routing system, search param handling, scroll restoration, middleware, and most advanced routing features. Waku's `Link` component supports basic `scroll` and `prefetchOnEnter`/`prefetchOnView` props, but most of its router API is still marked as `unstable_`. Waku is a good choice for simple RSC applications that don't need advanced routing.
267+
Waku is another RSC-native framework built on Vite, but takes a deliberately minimal approach. It provides basic file-based routing with layouts and RSC support, but lacks the typed routing system, search param handling, scroll restoration, middleware, and most advanced routing features. Waku's `Link` component supports basic `scroll` and `prefetchOnEnter`/`prefetchOnView` props, but most of its router API is still marked as `unstable_`. Waku is a good choice for simple RSC applications that don't need advanced routing.

docs/src/pages/en/(pages)/features/devtools.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ The Outlets tab lists every named outlet currently rendered on the page. Outlets
253253
|-------|---------|
254254
| Name | The outlet identifier |
255255
| URL | The URL the outlet content was loaded from (if applicable) |
256-
| Badge | How the outlet is managed: `router` (file-router), `remote` (fetched from another server), `live` (streaming), `defer` (lazily loaded), or `static` |
256+
| Badge | How the outlet is managed: `router` (file-router), `remote` (fetched from another server), `island` (hydration island), `live` (streaming), `defer` (lazily loaded), or `static`. Hydration islands also show `hydrated` or `not hydrated` so you can see whether the island has already become interactive. |
257257

258258
<Link name="highlighting-outlets-on-the-page">
259259
### Highlighting outlets on the page

0 commit comments

Comments
 (0)