From 53648592cd22517baf4b0511052d64c667c6de10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20L=C3=A1z=C3=A1r?= Date: Wed, 20 May 2026 14:04:30 +0200 Subject: [PATCH 1/2] feat: hydration islands --- docs/public/llms.txt | 2 + .../pages/en/(pages)/features/comparison.mdx | 111 ++- .../pages/en/(pages)/features/devtools.mdx | 2 +- .../en/(pages)/features/hydration-islands.mdx | 262 +++++++ docs/src/pages/en/features.(index).mdx | 2 +- .../pages/ja/(pages)/features/comparison.mdx | 111 ++- .../pages/ja/(pages)/features/devtools.mdx | 2 +- .../ja/(pages)/features/hydration-islands.mdx | 262 +++++++ docs/src/pages/ja/features.(index).mdx | 2 +- examples/hydration-islands/App.jsx | 442 ++++++++++++ examples/hydration-islands/Counter.jsx | 29 + examples/hydration-islands/RootClient.jsx | 33 + examples/hydration-islands/StrategyProbe.jsx | 34 + .../VisibilityLoadedClient.jsx | 35 + examples/hydration-islands/package.json | 13 + packages/react-server/README.md | 18 +- .../react-server/client/ClientProvider.jsx | 22 +- .../client/HydrationIslandBoundary.jsx | 164 +++++ .../client/ReactServerComponent.jsx | 53 +- packages/react-server/client/entry.client.jsx | 251 ++++++- .../client/hydration-island-data.mjs | 21 + .../client/hydration-island-runtime.mjs | 100 +++ .../react-server/client/script-templates.mjs | 22 + .../devtools/client/PayloadCollector.jsx | 20 +- .../devtools/client/panels/OutletPanel.jsx | 13 + packages/react-server/lib/build/edge.mjs | 3 + packages/react-server/lib/build/server.mjs | 8 + .../react-server/lib/dev/create-server.mjs | 14 +- .../strip-broken-dependency-sourcemaps.mjs | 44 ++ .../lib/plugins/use-directive-inline.mjs | 107 ++- .../lib/plugins/use-hydrate-inline.mjs | 232 ++++++ packages/react-server/lib/utils/manifest.mjs | 9 +- packages/react-server/server/dom-flight.mjs | 12 + .../react-server/server/hydration-island.jsx | 327 +++++++++ packages/react-server/server/render-dom.mjs | 108 ++- packages/react-server/server/render-rsc.jsx | 89 ++- packages/react-server/server/symbols.mjs | 1 + skills/react-server/SKILL.md | 29 + test/__test__/apps/hydration-islands.spec.mjs | 667 ++++++++++++++++++ 39 files changed, 3563 insertions(+), 113 deletions(-) create mode 100644 docs/src/pages/en/(pages)/features/hydration-islands.mdx create mode 100644 docs/src/pages/ja/(pages)/features/hydration-islands.mdx create mode 100644 examples/hydration-islands/App.jsx create mode 100644 examples/hydration-islands/Counter.jsx create mode 100644 examples/hydration-islands/RootClient.jsx create mode 100644 examples/hydration-islands/StrategyProbe.jsx create mode 100644 examples/hydration-islands/VisibilityLoadedClient.jsx create mode 100644 examples/hydration-islands/package.json create mode 100644 packages/react-server/client/HydrationIslandBoundary.jsx create mode 100644 packages/react-server/client/hydration-island-data.mjs create mode 100644 packages/react-server/client/hydration-island-runtime.mjs create mode 100644 packages/react-server/client/script-templates.mjs create mode 100644 packages/react-server/lib/plugins/strip-broken-dependency-sourcemaps.mjs create mode 100644 packages/react-server/lib/plugins/use-hydrate-inline.mjs create mode 100644 packages/react-server/server/hydration-island.jsx create mode 100644 test/__test__/apps/hydration-islands.spec.mjs diff --git a/docs/public/llms.txt b/docs/public/llms.txt index 3d217b0d..b374c240 100644 --- a/docs/public/llms.txt +++ b/docs/public/llms.txt @@ -32,6 +32,7 @@ No config files, no boilerplate, no separate React installation needed. Or use t - **React Server Components**: The default rendering model. Components render on the server, support async/await, and their source code never reaches the client. - **Client Components**: Add `"use client"` directive to enable interactivity. Components are server-rendered then hydrated on the client. +- **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. - **Server Functions**: Add `"use server"` to turn async functions into server-side RPC endpoints. Works as form actions with progressive enhancement. - **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. - **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 - [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 - [Cluster Mode](https://react-server.dev/features/cluster): Running the production server in multi-process cluster mode to utilize all CPU cores - [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 +- [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 - [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 - [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 - [Micro-Frontends](https://react-server.dev/features/micro-frontends): Implementing micro-frontend architecture with RemoteComponent for loading and rendering remote React applications with SSR diff --git a/docs/src/pages/en/(pages)/features/comparison.mdx b/docs/src/pages/en/(pages)/features/comparison.mdx index f7ec0cac..e1562213 100644 --- a/docs/src/pages/en/(pages)/features/comparison.mdx +++ b/docs/src/pages/en/(pages)/features/comparison.mdx @@ -27,13 +27,14 @@ This comparison summarizes the feature set described in the documentation. It fo | | @lazarv/react-server | Next.js | TanStack Start | React Router | Waku | |--|:---:|:---:|:---:|:---:|:---:| -| React Server Components | ✅ | ✅ | 🛑 | 🟡 | ✅ | +| React Server Components | ✅ | ✅ | 🟡 | 🟡 | ✅ | | Server Functions (actions) | ✅ | ✅ | ✅ | 🟡 | ✅ | +| Server Function Defense Layer | ✅
*encrypted refs + hardened decoder* | 🟡
*IDs/origin/body cap* | 🟡
*CSRF + validators* | 🔶
*route action model* | 🔶
*auth/userland* | | SSR | ✅ | ✅ | ✅ | ✅ | ✅ | | Streaming SSR | ✅ | ✅ | ✅ | ✅ | ✅ | | API Routes | ✅ | ✅ | ✅ | ✅ | ✅ | | API / Route Middleware | ✅ | ✅ | ✅ | ✅ | 🟡 | -| Server Function Middleware | 🟡 | 🛑 | ✅ | 🛑 | 🛑 | +| Server Function Middleware | 🔶
*not first-class* | 🛑 | ✅
*client/server chain* | 🔶
*not first-class* | 🛑 | | Static Site Generation | ✅ | ✅ | 🟡 | 🟡 | ✅ | | Partial Pre-rendering (PPR) | ✅ | ✅ | 🛑 | 🛑 | 🛑 | | Response Caching (TTL) | ✅ | ✅ | 🛑 | 🛑 | 🛑 | @@ -49,6 +50,9 @@ This comparison summarizes the feature set described in the documentation. It fo |--|:---:|:---:|:---:|:---:|:---:| | Open Runtime (no vendor lock-in) | ✅ | 🟡
*optimized for Vercel* | ✅ | ✅ | ✅ | | Vite-based | ✅ | 🛑 | ✅ | ✅ | ✅ | +| Production HTTP Layer | ✅
*backpressure + security* | 🟡
*platform-managed* | 🔶
*host/adapter* | 🔶
*host/adapter* | 🔶
*host/adapter* | +| Observability | ✅
*OTel traces + metrics* | ✅
*OTel spans* | 🟡
*manual/experimental* | 🟡
*instrumentation API* | 🔶
*userland* | +| Hydration Islands | ✅ | 🛑 | 🟡
*deferred tree only* | 🛑 | 🛑 | | Multiple Deploy Targets | ✅ | 🟡 | ✅ | ✅ | ✅ | | Micro-frontend / Remote Components | ✅ | 🔶 | 🛑 | 🛑 | 🛑 | | MCP Server Integration | ✅ | 🛑 | 🛑 | 🛑 | 🛑 | @@ -158,9 +162,9 @@ This comparison summarizes the feature set described in the documentation. It fo | Virtual Routes Module | ✅ | 🛑 | 🟡 | 🟡 | 🛑 | | Route-scoped Loading / Error / Fallback Files | ✅ | ✅ | 🛑 | 🛑 | 🛑 | | Devtools | ✅ | 🛑 | ✅
*router only* | 🟡 | 🛑 | -| Route Masking | 🛑 | 🛑 | ✅ | 🛑 | 🛑 | +| Route Masking | 🔶
*URL identity model* | 🛑 | ✅ | 🛑 | 🛑 | | Route-level Typed Dependencies | ✅
*Typesafe resources* | 🛑 | ✅
*Typesafe route context + loaders* | 🛑 | 🛑 | -| Route Mount / Unmount Events | 🛑 | 🛑 | ✅ | 🛑 | 🛑 | +| Route Lifecycle Hooks | 🔶
*guards + React lifecycle* | 🛑 | ✅ | 🛑 | 🛑 | ## Notes @@ -168,13 +172,96 @@ This comparison summarizes the feature set described in the documentation. It fo `@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. + ### Key architectural differences + + + +#### RSC-native runtime + + +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. + + +#### Production HTTP runtime + + +`@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. + + +#### Server function defense layer + + +`@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. + + +#### Server function middleware model + + +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. + + +#### Observability runtime + + +`@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. + + +#### 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 `` component. + + +#### 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. + + +#### 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. + + +#### 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. + + +#### 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. + + +#### 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. + + +#### Route identity over route masking + + +`@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. + + +#### Route lifecycle model + + +`@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. + + +#### Hydration islands vs. deferred hydration + + +`@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. + + +#### Waku + -- **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. -- **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 `` component. -- **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. -- **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. -- **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. -- **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. -- **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. -- **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. +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. diff --git a/docs/src/pages/en/(pages)/features/devtools.mdx b/docs/src/pages/en/(pages)/features/devtools.mdx index 0e634bb1..c49bbdd6 100644 --- a/docs/src/pages/en/(pages)/features/devtools.mdx +++ b/docs/src/pages/en/(pages)/features/devtools.mdx @@ -253,7 +253,7 @@ The Outlets tab lists every named outlet currently rendered on the page. Outlets |-------|---------| | Name | The outlet identifier | | URL | The URL the outlet content was loaded from (if applicable) | -| Badge | How the outlet is managed: `router` (file-router), `remote` (fetched from another server), `live` (streaming), `defer` (lazily loaded), or `static` | +| 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. | ### Highlighting outlets on the page diff --git a/docs/src/pages/en/(pages)/features/hydration-islands.mdx b/docs/src/pages/en/(pages)/features/hydration-islands.mdx new file mode 100644 index 00000000..12362e75 --- /dev/null +++ b/docs/src/pages/en/(pages)/features/hydration-islands.mdx @@ -0,0 +1,262 @@ +--- +title: Hydration Islands +category: Features +order: 6 +--- + +import Link from "../../../../components/Link.jsx"; + +# Hydration Islands + +Hydration islands let a mostly static server-rendered page hydrate only selected subtrees. Mark a component with the `"use hydrate"` directive and the runtime renders that subtree as normal server HTML, writes a separate RSC payload for the island, and hydrates it later as a local non-root outlet. + +This is different from a normal `"use client"` component. A client component still participates in the page's main React tree. A hydration island creates its own local outlet and can hydrate even when the page root has no client components and no `PAGE_ROOT` RSC payload. + +Hydration islands are an initial HTML rendering strategy. If a component marked with `"use hydrate"` is rendered later as part of an RSC update payload, it returns its normal component output instead of creating a new island, because that subtree is already owned by the existing React tree. + + +## Islands vs PPR + + +Hydration islands and [partial pre-rendering](/features/ppr) solve different parts of the rendering pipeline. + +| Feature | Controls | Output | +|---------|----------|--------| +| Partial pre-rendering | Whether a route segment is rendered at build time or at request time | Static HTML plus streamed dynamic server content | +| Hydration islands | Whether a server-rendered subtree gets its own deferred client hydration boundary | Static HTML plus an island-specific RSC payload and local outlet | + +Use PPR when you want a mostly static page shell with request-time server content inside it. Use hydration islands when the server-rendered HTML should become interactive later without hydrating the page root. The two features can be combined: a PPR page can contain an island, and the island still owns its own hydration strategy and local outlet navigation. + + +## Creating an island + + +Use `"use hydrate: "` inside a component body. The directive is lexically scoped, like inline `"use client"` and `"use server"` functions. + +```jsx +import { useState } from "react"; + +function Counter() { + "use client"; + + const [count, setCount] = useState(0); + return ; +} + +function CounterIsland() { + "use hydrate: idle; timeout=1200; id=counter"; + + return ; +} + +export default function App() { + return ( +
+

Server-rendered page

+ +
+ ); +} +``` + +The page root above stays server-only. The island receives server-rendered HTML immediately, then hydrates according to its strategy. + + +## Strategies + + +The directive uses a colon for the strategy and semicolon-separated parameters: + +```jsx +"use hydrate: idle; timeout=1200; id=counter"; +``` + +Supported strategies: + +| Strategy | Behavior | +|----------|----------| +| `load` | Hydrates as soon as the client entry runs | +| `idle` | Hydrates through `requestIdleCallback`, falling back to `setTimeout` | +| `visible` | Hydrates when the island marker intersects the viewport | +| `interaction` | Hydrates on user interaction events | +| `media` | Hydrates when a media query matches | +| `never` | Renders HTML but does not hydrate | + + +### `load` + + +`load` hydrates the island as soon as the client entry has started and the island payload is available. This is the default when no strategy is specified, so `"use hydrate"` and `"use hydrate: load"` both describe an eager island. + +Use `load` for above-the-fold UI that should become interactive immediately, while still keeping the page root static or keeping the island in a separate local outlet. + +```jsx +function SearchIsland() { + "use hydrate: load; id=search"; + + return ; +} +``` + +The island still uses a separate RSC payload and outlet. It is eager only in the sense that the browser starts hydrating it during startup instead of waiting for an idle callback, viewport event, user interaction, or media query. + + +### `idle` + + +`idle` waits until the browser reports idle time through `requestIdleCallback`. If `requestIdleCallback` is not available, the runtime falls back to `setTimeout`. The optional `timeout` parameter controls the maximum delay in milliseconds and defaults to `2000`. + +Use `idle` for interactive UI that is useful soon after load but not required for the first paint, such as secondary counters, filters below the first viewport, preference panels, or low-priority widgets. + +```jsx +function FiltersIsland() { + "use hydrate: idle; timeout=1200; id=filters"; + + return ; +} +``` + +An idle island is server-rendered immediately. It is not interactive until the idle task runs and hydration completes, so avoid this strategy for controls that users are likely to click immediately. + + +### `visible` + + +`visible` uses `IntersectionObserver` and hydrates when the island wrapper intersects the viewport. Use `rootMargin` to start hydration before the island is actually visible, and `threshold` to require a minimum visible ratio before hydration starts. The default `rootMargin` is `600px`; the default `threshold` is `0`. + +Use `visible` for content that is below the fold or hidden in long pages. It keeps the client modules for deferred-only island components out of the initial module preload set, then loads and hydrates them when the island approaches the viewport. + +```jsx +function VisibleIsland() { + "use hydrate: visible; rootMargin=0px; threshold=0.2; id=visible_counter"; + + return ; +} +``` + +If `IntersectionObserver` is unavailable, the runtime hydrates immediately instead of leaving the UI permanently inert. + + +### `interaction` + + +`interaction` hydrates when the user interacts with the island wrapper. By default it listens for `pointerenter`, `focusin`, `pointerdown`, and `click`. Pass an `events` parameter to replace that list with a comma-separated set of DOM events. + +Use `interaction` for UI that should stay static until the user shows intent, such as menus, popovers, expandable panels, rating controls, or expensive controls that are unlikely to be used on every page view. + +```jsx +function MenuIsland() { + "use hydrate: interaction; events=pointerenter,focusin; id=account_menu"; + + return ; +} +``` + +The first matching event starts hydration. Do not rely on that same event being replayed into the newly hydrated component. For controls where the first click must run the component's own `onClick` handler, prefer an earlier intent event such as `pointerenter` or `focusin`, or use `load` instead. + + +### `media` + + +`media` hydrates when a `matchMedia` query matches. Pass the query with the `query` parameter. If the query already matches when the client entry runs, the island hydrates immediately. If it does not match yet, the runtime listens for media query changes. + +Use `media` for responsive UI that only needs client behavior in certain environments, such as desktop-only toolbars, wide-screen inspectors, reduced-motion alternatives, or controls that should hydrate only for pointer-capable layouts. + +```jsx +function DesktopIsland() { + "use hydrate: media; query=(min-width: 900px); id=desktop_tools"; + + return ; +} +``` + +Motion preferences should also use `media`, because `prefers-reduced-motion` is a standard media query. For example, hydrate an animation-heavy widget only when the user has not requested reduced motion: + +```jsx +function MotionIsland() { + "use hydrate: media; query=(prefers-reduced-motion: no-preference); id=motion"; + + return ; +} +``` + +You can also invert the query and hydrate a reduced-motion-specific control only for users who requested it: + +```jsx +function ReducedMotionIsland() { + "use hydrate: media; query=(prefers-reduced-motion: reduce); id=reduced_motion"; + + return ; +} +``` + +If `matchMedia` is unavailable, or if the directive does not include a `query`, the runtime hydrates immediately. + + +### `never` + + +`never` renders the island's HTML but does not create a hydration payload and does not hydrate the island. The result is static server-rendered markup. + +Use `never` when the same component shape is useful in a strategy matrix or when you want an explicit static island boundary for content that must never become interactive. It is also useful as a test fixture because it proves that the runtime can emit island markup without request-scoped hydration data. + +```jsx +function StaticPromoIsland() { + "use hydrate: never; id=static_promo"; + + return ; +} +``` + +Because there is no hydration payload, client components inside a `never` island are only represented by their server-rendered HTML. Event handlers and effects never run. + + +## Local outlet navigation + + +An island is hydrated as a local non-root outlet. Components inside the island can use `Link local` and `Refresh local` to update that island without hydrating or navigating the page root. + +```jsx +import { useSearchParams, useUrl } from "@lazarv/react-server"; +import { Link, Refresh } from "@lazarv/react-server/navigation"; + +function NavigationIsland() { + "use hydrate: load; id=rsc_navigation"; + + const url = useUrl(); + const search = useSearchParams(); + const view = search?.view === "details" ? "details" : "overview"; + + return ( +
+

{url.pathname}{url.search}

+

{view}

+ Overview + Details + Refresh island +
+ ); +} +``` + +`Link local` fetches the island's `@.rsc.x-component` payload and swaps only that outlet. The browser URL and `PAGE_ROOT` hydration state are left alone unless you explicitly target the root. + +Hydration islands participate in client navigation only after they hydrate. An island that has not hydrated yet is not registered as a client outlet, so it will not receive `Link`, `Refresh`, `pushstate`, `replacestate`, or `popstate` updates. When the island hydrates later, it starts from the server-rendered island payload and the current browser location. + +If an island navigation explicitly opts into browser history with `push` or `replace`, browser back and forward are still global browser events. Hydrated outlets can react to those events together. Islands that hydrate after earlier browser history events do not replay those missed events. + + +## DevTools + + +When DevTools are enabled, hydration islands appear in the Outlets panel with an `island` badge. The panel also shows whether an island has already hydrated. Islands are local outlets, not remote components, so they do not appear in the Remotes panel. + + +## Example + + +The repository includes a complete example: + +```sh +pnpm --filter ./examples/hydration-islands dev --open +``` diff --git a/docs/src/pages/en/features.(index).mdx b/docs/src/pages/en/features.(index).mdx index 35480435..e9eea3fe 100644 --- a/docs/src/pages/en/features.(index).mdx +++ b/docs/src/pages/en/features.(index).mdx @@ -14,7 +14,7 @@ The runtime encrypts all [server function identifiers](/features/server-function For error handling, you can learn about how to use the built-in [error boundary](/features/error-handling) component and how to implement your own error handling strategy. -You can also learn about some small, but useful modes in this section, like [partial pre-rendering](/features/ppr), [cluster mode](/features/cluster) or [middleware mode](/features/middleware-mode). Partial pre-rendering is useful when you want to pre-render only parts of your app. Cluster mode is useful when you want to run your app in a multi-process environment. While middleware mode is useful when you want to run your app as a middleware in an existing server, like Express or NestJS. +You can also learn about some small, but useful modes in this section, like [partial pre-rendering](/features/ppr), [hydration islands](/features/hydration-islands), [cluster mode](/features/cluster) or [middleware mode](/features/middleware-mode). Partial pre-rendering is useful when you want to pre-render only parts of your app. Hydration islands let you keep a server-rendered root static while hydrating selected subtrees as local outlets. Cluster mode is useful when you want to run your app in a multi-process environment. While middleware mode is useful when you want to run your app as a middleware in an existing server, like Express or NestJS. You can learn about how to implement a micro-frontend architecture in the [micro-frontends](/features/micro-frontends) section. The runtime provides a set of tools to help you implement micro-frontends in your app. You can use the `RemoteComponent` component to load a micro-frontend from a remote URL and render it in your app using server-side rendering. Server-side rendering supported `iframe` fragments for React applications! diff --git a/docs/src/pages/ja/(pages)/features/comparison.mdx b/docs/src/pages/ja/(pages)/features/comparison.mdx index 9f1c7100..d1da5968 100644 --- a/docs/src/pages/ja/(pages)/features/comparison.mdx +++ b/docs/src/pages/ja/(pages)/features/comparison.mdx @@ -27,13 +27,14 @@ import Link from "../../../../components/Link.jsx"; | | @lazarv/react-server | Next.js | TanStack Start | React Router | Waku | |--|:---:|:---:|:---:|:---:|:---:| -| React Server Components | ✅ | ✅ | 🛑 | 🟡 | ✅ | +| React Server Components | ✅ | ✅ | 🟡 | 🟡 | ✅ | | サヌバヌ関数アクション | ✅ | ✅ | ✅ | 🟡 | ✅ | +| サヌバヌ関数防埡レむダヌ | ✅
*暗号化ref + hardened decoder* | 🟡
*ID/origin/body侊限* | 🟡
*CSRF + validator* | 🔶
*route actionモデル* | 🔶
*auth/userland* | | SSR | ✅ | ✅ | ✅ | ✅ | ✅ | | ストリヌミングSSR | ✅ | ✅ | ✅ | ✅ | ✅ | | APIルヌト | ✅ | ✅ | ✅ | ✅ | ✅ | | API / ルヌトミドルりェア | ✅ | ✅ | ✅ | ✅ | 🟡 | -| サヌバヌ関数ミドルりェア | 🟡 | 🛑 | ✅ | 🛑 | 🛑 | +| サヌバヌ関数ミドルりェア | 🔶
*not first-class* | 🛑 | ✅
*client/server chain* | 🔶
*not first-class* | 🛑 | | 静的サむト生成 | ✅ | ✅ | 🟡 | 🟡 | ✅ | | 郚分的プリレンダリングPPR | ✅ | ✅ | 🛑 | 🛑 | 🛑 | | レスポンスキャッシュTTL | ✅ | ✅ | 🛑 | 🛑 | 🛑 | @@ -49,6 +50,9 @@ import Link from "../../../../components/Link.jsx"; |--|:---:|:---:|:---:|:---:|:---:| | オヌプンランタむムベンダヌロックむンなし | ✅ | 🟡
*Vercel向けに最適化* | ✅ | ✅ | ✅ | | Viteベヌス | ✅ | 🛑 | ✅ | ✅ | ✅ | +| 本番HTTPレむダヌ | ✅
*バックプレッシャヌ + セキュリティ* | 🟡
*プラットフォヌム管理* | 🔶
*ホスト/アダプタヌ* | 🔶
*ホスト/アダプタヌ* | 🔶
*ホスト/アダプタヌ* | +| オブザヌバビリティ | ✅
*OTelトレヌス + メトリクス* | ✅
*OTelスパン* | 🟡
*手動/実隓的* | 🟡
*蚈枬API* | 🔶
*ナヌザヌランド* | +| Hydration Islands | ✅ | 🛑 | 🟡
*遅延Reactツリヌのみ* | 🛑 | 🛑 | | 耇数のデプロむタヌゲット | ✅ | 🟡 | ✅ | ✅ | ✅ | | マむクロフロント゚ンド / リモヌトコンポヌネント | ✅ | 🔶 | 🛑 | 🛑 | 🛑 | | MCPサヌバヌ統合 | ✅ | 🛑 | 🛑 | 🛑 | 🛑 | @@ -158,9 +162,9 @@ import Link from "../../../../components/Link.jsx"; | 仮想ルヌトモゞュヌル | ✅ | 🛑 | 🟡 | 🟡 | 🛑 | | ルヌトスコヌプのロヌディング / ゚ラヌ / フォヌルバックファむル | ✅ | ✅ | 🛑 | 🛑 | 🛑 | | ルヌトDevtools | 🛑 | 🛑 | ✅ | 🟡 | 🛑 | -| ルヌトマスキング | 🛑 | 🛑 | ✅ | 🛑 | 🛑 | +| ルヌトマスキング | 🔶
*URL IDモデル* | 🛑 | ✅ | 🛑 | 🛑 | | ルヌトレベルの型付き䟝存関係 | ✅
*型安党なリ゜ヌス* | 🛑 | ✅
*型安党なルヌトコンテキスト + ロヌダヌ* | 🛑 | 🛑 | -| ルヌトのマりント / アンマりントむベント | 🛑 | 🛑 | ✅ | 🛑 | 🛑 | +| ルヌトラむフサむクルフック | 🔶
*ガヌド + Reactラむフサむクル* | 🛑 | ✅ | 🛑 | 🛑 | ## 泚蚘 @@ -168,13 +172,96 @@ import Link from "../../../../components/Link.jsx"; `@lazarv/react-server`は完党なReact Server Componentsランタむムであり、単なるルヌタヌではありたせん。ルヌティングシステムはRSCストリヌミング、サヌバヌ関数、Viteビルドパむプラむンず深く統合されおいたす。これにより、型付きルヌト、クラむアント専甚ルヌト、サヌバヌサむドバリデヌションなどの機胜が、グルヌコヌドなしで゚ンドツヌ゚ンドで動䜜したす。 + ### 䞻なアヌキテクチャの違い + + + +#### RSCネむティブランタむム + + +RSCサポヌトを埌付けするルヌタヌずは異なり、`@lazarv/react-server`はReact Server Componentsのために䞀から構築されおいたす。すべおのルヌトでサヌバヌコンポヌネントずクラむアントコンポヌネントを自由に混圚させるこずができたす。 + + +#### 本番HTTPランタむム + + +`@lazarv/react-server`は、keep-alive/timeout調敎、アドミッション制埡、ELUベヌスのアダプティブバックプレッシャヌ、request/bodyずmultipartの䞊限、action POST向けCSRF origin怜蚌、health/readiness゚ンドポむント、graceful shutdownを備えた本番ランタむムを持ちたす。他のReactスタックでは倚くの堎合、これらはファヌストクラスのランタむム局ではなく、ホスティングプラットフォヌム、カスタムサヌバヌ、リバヌスプロキシ、アダプタヌ偎に委ねられたす。 + + +#### サヌバヌ関数防埡レむダヌ + + +`@lazarv/react-server`は、アプリケヌションコヌドを実行する前にサヌバヌ関数呌び出しを敵察的入力ずしお扱いたす。サヌバヌ関数参照はAES-256-GCMのcapability tokenで暗号化され、key rotationをサポヌトし、`maxRows`、`maxDepth`、`maxBytes`、string/BigInt/streamの䞊限などの蚭定可胜なリ゜ヌス制限ず、prototype pollution、犁止されたpath walk、悪意あるthenable、停造callableを防ぐ構造的防埡を通しおデコヌドされたす。Next.jsにもsecure ID、origin check、closure encryption、body size capなどの意味のあるServer Action防埡がありたす。TanStack StartにはCSRF middlewareずvalidatorがありたす。`react-server`で特城的なのは、capability-tokenモデルずhardened RSC reply decoderを組み合わせたレむダヌが、ファヌストクラスのランタむム境界になっおいる点です。 + + +#### サヌバヌ関数ミドルりェアモデル + + +この比范では、TanStack Start が最も盎接的なサヌバヌ関数ミドルりェアモデルを持ちたす。function middleware は、client偎ロゞック、server偎ロゞック、input validation、composition、global registration でサヌバヌ関数を包めたす。`@lazarv/react-server`は珟時点では TanStack 圢匏のファヌストクラスな関数単䜍 middleware chain を公開しおいたせん。request middleware はサヌバヌ関数ディスパッチの前に実行され、リク゚ストを短絡したり泚釈付けしたりできたす。䞀方で `createFunction` は、ハンドラ実行前にプロトコル局で匕数ごずの parse/validate contract を提䟛したす。関数暪断のポリシヌはナヌザヌランドの wrapper でも構成できたすが、ファヌストクラスのランタむムプリミティブは client/server function middleware pipeline ではなく、request middleware ず contract/decoder 境界です。React Router の middleware は route `action`/`loader` リク゚ストを包むもので有甚ですが、任意のRPCサヌバヌ関数ではなく route action モデルに玐づくため、この行では同じくファヌストクラスサポヌトではありたせん。 + + +#### オブザヌバビリティランタむム + + +`@lazarv/react-server`は組み蟌みOpenTelemetry統合を持ち、HTTPリク゚スト、ミドルりェア、RSC/SSRレンダリング、サヌバヌ関数、キャッシュ怜玢、サヌバヌ起動、Vite開発フックを自動蚈枬できたす。request、render、server function、cacheの組み蟌みメトリクスも提䟛したす。テレメトリ䟝存関係はオプションか぀遅延読み蟌みのため、無効時はno-op蚈枬に解決されたす。Next.jsにも公匏のOpenTelemetry蚈枬ずフレヌムワヌクスパンがありたす。React Routerはログやトレヌスに接続できるファヌストクラスの蚈枬ラッパヌを提䟛したす。TanStack Startはobservabilityパタヌンず手動/実隓的なOpenTelemetryセットアップを文曞化しおおり、Wakuは䞻にナヌザヌランドたたはホストプラットフォヌム偎に委ねるモデルです。 + + +#### クラむアント専甚ルヌト + + +ファむルシステムルヌタヌで`"use client"`を持぀ペヌゞは自動的にクラむアント専甚ルヌトになりたす — ナビゲヌションはサヌバヌを完党にスキップしたす。蚭定は䞍芁で、Reactの``コンポヌネントによりコンポヌネントの状態はナビゲヌション間で保持されたす。 + + +#### スキヌマ非䟝存のバリデヌション + + +ルヌトパラメヌタず怜玢パラメヌタは、任意のスキヌマラむブラリZod、ArkType、Valibotたたは軜量パヌス関数でバリデヌションできたす — ランタむムがバリデヌション戊略を自動的に怜出したす。 + + +#### RSC + 型付きリ゜ヌス + + +`@lazarv/react-server`は2぀の補完的なデヌタ取埗アプロヌチを提䟛したす`async/await`によるReact Server ComponentsRSCをロヌダヌずしおず、**型付きリ゜ヌス** — `.use()`Suspense、`.query()`呜什型、`.prefetch()`、`.invalidate()`を備えたスキヌマバリデヌション枈みの参照識別デヌタディスクリプタです。リ゜ヌスはキャッシュランタむムずしお`"use cache"`を䜿甚したす — カスタムキャッシュレむダヌやSWRのボむラヌプレヌトは䞍芁です。ルヌト-リ゜ヌスバむンディングにより、ナビゲヌション時の䞊列プリフェッチが可胜になりたす。 + + +#### ルヌトレベルの䟝存関係 + + +TanStack Routerはルヌトコンテキストを、䟝存関係認蚌、DBクラむアントなどをルヌトツリヌに枡すためのファヌストクラスの型付きプリミティブずしお公開しおいたす。`@lazarv/react-server`は同じ問題領域を型安党なリ゜ヌス、リク゚ストコンテキスト、ネむティブモゞュヌルでモデル化しおいたす — リ゜ヌスがすでにスキヌマバリデヌション枈みのルヌトスコヌプデヌタを完党な型安党性で提䟛するため、別のルヌトコンテキストバッグは䞍芁です。 + + +#### 組み蟌みDevtools + + +`@lazarv/react-server`は統合Devtools`--devtools`を提䟛したす。ルヌト怜査だけでなく、RSCペむロヌド、キャッシュ゚ントリ、ルヌト、アりトレット、リモヌトコンポヌネント、live components、workers、server logsを単䞀パネルで扱いたす。TanStack RouterのDevtoolsはルヌタヌに特化した優れたものですが、`@lazarv/react-server`のDevtoolsはサヌバヌコンポヌネントランタむム党䜓を察象にしたす。 + + +#### アりトレットずパラレルルヌト + + +`@lazarv/react-server`は名前付きアりトレット`@sidebar`、`@content`を䜿甚し、レむアりトに型付きプロップずしおレンダリングしたす。これはNext.jsのパラレルルヌトず機胜的に類䌌しおいたすが、より匷い型付けを備えおいたす — 各アりトレットはブランド付きReact芁玠であり、誀っおアりトレットを入れ替えるこずを防ぎたす。 + + +#### ルヌトマスキングよりもルヌトIDの䞀貫性 + + +`@lazarv/react-server`は TanStack 圢匏のルヌトマスキングを意図的にサポヌトしおいたせん。ブラりザに衚瀺されるURL、サヌバヌリク゚ストURL、マッチしたルヌトツリヌ、RSCペむロヌド、ルヌトバリデヌション、リ゜ヌス、キャッシュキヌ、アりトレット、Devtoolsの状態は、同じナビゲヌション状態を衚すこずを前提にしおいたす。URLに別の芋せ方が必芁な堎合は、隠れたクラむアント専甚のルヌトIDを䜜るのではなく、rewrite、redirect、ルヌトグルヌプ、アりトレット、怜玢パラメヌタを䜿いたす。 + + +#### ルヌトラむフサむクルモデル + + +`@lazarv/react-server`は、クラむアント偎のペヌゞルヌトナビゲヌション刀断のために `useNavigationGuard` ず `registerNavigationGuard` を提䟛したす。back/forward ず任意の `beforeUnload` 凊理も含みたす。内郚では、ルヌトガヌド/登録コンポヌネントがルヌト登録、アクティブ衚瀺、ルヌトリ゜ヌス、ロヌディングスケルトン、`Activity` による状態保持を管理したす。ナヌザヌ偎の副䜜甚は、ルヌタヌオブゞェクトの mount/unmount コヌルバックではなく、React effects、resources、middleware、server functions、cache invalidation に眮くモデルです。 + + +#### Hydration Islands ず deferred hydration の違い + + +`@lazarv/react-server`の Hydration Islands は初回レンダリングの境界です。サヌバヌHTMLをすぐに描画し、リク゚ストスコヌプの島専甚ハむドレヌションデヌタを保持し、埌から独立したロヌカルアりトレットずしおハむドレヌトしたす。これは `PAGE_ROOT` をハむドレヌトしないペヌゞでも動䜜したす。TanStack Start には Suspense されたReactサブツリヌの deferred hydration 的な動䜜がありたすが、そのサブツリヌはペヌゞのReactツリヌ内に残り、独自のRSCペむロヌドを持぀別の島アりトレットにはなりたせん。Astro はこの React/RSC 比范の倖では最も近いアヌキテクチャです。コンポヌネント単䜍のハむドレヌションに察する本物の Islands アヌキテクチャを持ちたすが、RSC のロヌカルアりトレットモデルではありたせん。他のReactフレヌムワヌクでもナヌザヌランドでクラむアントコンポヌネントの遅延読み蟌みやJavaScriptの遅延は可胜ですが、このロヌカルアりトレット型の島モデルをファヌストクラス機胜ずしおは提䟛しおいたせん。 + + +#### Waku + -- **RSCネむティブ**: RSCサポヌトを埌付けするルヌタヌずは異なり、`@lazarv/react-server`はReact Server Componentsのために䞀から構築されおいたす。すべおのルヌトでサヌバヌコンポヌネントずクラむアントコンポヌネントを自由に混圚させるこずができたす。 -- **クラむアント専甚ルヌト**: ファむルシステムルヌタヌで`"use client"`を持぀ペヌゞは自動的にクラむアント専甚ルヌトになりたす — ナビゲヌションはサヌバヌを完党にスキップしたす。蚭定は䞍芁で、Reactの``コンポヌネントによりコンポヌネントの状態はナビゲヌション間で保持されたす。 -- **スキヌマ非䟝存のバリデヌション**: ルヌトパラメヌタず怜玢パラメヌタは、任意のスキヌマラむブラリZod、ArkType、Valibotたたは軜量パヌス関数でバリデヌションできたす — ランタむムがバリデヌション戊略を自動的に怜出したす。 -- **RSC + 型付きリ゜ヌス**: `@lazarv/react-server`は2぀の補完的なデヌタ取埗アプロヌチを提䟛したす`async/await`によるReact Server ComponentsRSCをロヌダヌずしおず、**型付きリ゜ヌス** — `.use()`Suspense、`.query()`呜什型、`.prefetch()`、`.invalidate()`を備えたスキヌマバリデヌション枈みの参照識別デヌタディスクリプタです。リ゜ヌスはキャッシュランタむムずしお`"use cache"`を䜿甚したす — カスタムキャッシュレむダヌやSWRのボむラヌプレヌトは䞍芁です。ルヌト-リ゜ヌスバむンディングにより、ナビゲヌション時の䞊列プリフェッチが可胜になりたす。 -- **ルヌトレベルの䟝存関係**: TanStack Routerはルヌトコンテキストを、䟝存関係認蚌、DBクラむアントなどをルヌトツリヌに枡すためのファヌストクラスの型付きプリミティブずしお公開しおいたす。`@lazarv/react-server`は同じ問題領域を型安党なリ゜ヌス、リク゚ストコンテキスト、ネむティブモゞュヌルでモデル化しおいたす — リ゜ヌスがすでにスキヌマバリデヌション枈みのルヌトスコヌプデヌタを完党な型安党性で提䟛するため、別のルヌトコンテキストバッグは䞍芁です。 -- **Devtoolsはただありたせん**: ルヌトDevtoolsは蚈画䞭です。珟圚のトレヌドオフは、型付きルヌトシステムがコンパむル時の安党性を提䟛し、ほずんどのルヌティング゚ラヌをブラりザに到達する前にキャッチするこずです。 -- **アりトレットずパラレルルヌト**: `@lazarv/react-server`は名前付きアりトレット`@sidebar`、`@content`を䜿甚し、レむアりトに型付きプロップずしおレンダリングしたす。これはNext.jsのパラレルルヌトず機胜的に類䌌しおいたすが、より匷い型付けを備えおいたす — 各アりトレットはブランド付きReact芁玠であり、誀っおアりトレットを入れ替えるこずを防ぎたす。 -- **Waku**: WakuはVite䞊に構築されたもう䞀぀のRSCネむティブフレヌムワヌクですが、意図的にミニマルなアプロヌチを取っおいたす。基本的なファむルベヌスのルヌティングずレむアりト、RSCサポヌトを提䟛したすが、型付きルヌティングシステム、怜玢パラメヌタ凊理、スクロヌル埩元、ミドルりェア、その他の高床なルヌティング機胜は備えおいたせん。Wakuの`Link`コンポヌネントは基本的な`scroll`ず`prefetchOnEnter`/`prefetchOnView`プロップをサポヌトしおいたすが、ルヌタヌAPIのほずんどはただ`unstable_`マヌクが付いおいたす。Wakuは高床なルヌティングを必芁ずしないシンプルなRSCアプリケヌションに適しおいたす。 +WakuはVite䞊に構築されたもう䞀぀のRSCネむティブフレヌムワヌクですが、意図的にミニマルなアプロヌチを取っおいたす。基本的なファむルベヌスのルヌティングずレむアりト、RSCサポヌトを提䟛したすが、型付きルヌティングシステム、怜玢パラメヌタ凊理、スクロヌル埩元、ミドルりェア、その他の高床なルヌティング機胜は備えおいたせん。Wakuの`Link`コンポヌネントは基本的な`scroll`ず`prefetchOnEnter`/`prefetchOnView`プロップをサポヌトしおいたすが、ルヌタヌAPIのほずんどはただ`unstable_`マヌクが付いおいたす。Wakuは高床なルヌティングを必芁ずしないシンプルなRSCアプリケヌションに適しおいたす。 diff --git a/docs/src/pages/ja/(pages)/features/devtools.mdx b/docs/src/pages/ja/(pages)/features/devtools.mdx index b14cdea4..45198202 100644 --- a/docs/src/pages/ja/(pages)/features/devtools.mdx +++ b/docs/src/pages/ja/(pages)/features/devtools.mdx @@ -253,7 +253,7 @@ Outletsタブはペヌゞ䞊に珟圚レンダリングされおいるすべお |-----------|---------| | Name | アりトレット識別子 | | URL | アりトレットコンテンツのロヌド元URL該圓する堎合 | -| Badge | アりトレットの管理方法`router`ファむルルヌタヌ、`remote`別のサヌバヌから取埗、`live`ストリヌミング、`defer`遅延ロヌド、`static` | +| Badge | アりトレットの管理方法`router`ファむルルヌタヌ、`remote`別のサヌバヌから取埗、`island`Hydration Island、`live`ストリヌミング、`defer`遅延ロヌド、`static`。Hydration Island には `hydrated` たたは `not hydrated` も衚瀺され、すでにむンタラクティブになっおいるかどうかを確認できたす。 | ### ペヌゞ䞊のアりトレットをハむラむトする diff --git a/docs/src/pages/ja/(pages)/features/hydration-islands.mdx b/docs/src/pages/ja/(pages)/features/hydration-islands.mdx new file mode 100644 index 00000000..13c38c3f --- /dev/null +++ b/docs/src/pages/ja/(pages)/features/hydration-islands.mdx @@ -0,0 +1,262 @@ +--- +title: Hydration Islands +category: Features +order: 6 +--- + +import Link from "../../../../components/Link.jsx"; + +# Hydration Islands + +Hydration Islands は、ほずんどが静的なサヌバヌレンダリングペヌゞの䞭で、指定したサブツリヌだけを埌からハむドレヌトするための機胜です。コンポヌネントに `"use hydrate"` ディレクティブを付けるず、そのサブツリヌは通垞のサヌバヌHTMLずしお描画され、島専甚のRSCペむロヌドが曞き出され、ロヌカルの非ルヌトアりトレットずしお埌からハむドレヌトされたす。 + +これは通垞の `"use client"` コンポヌネントずは異なりたす。クラむアントコンポヌネントはペヌゞのメむンReactツリヌに参加したすが、Hydration Island は独自のロヌカルアりトレットを䜜成したす。そのため、ペヌゞルヌトにクラむアントコンポヌネントや `PAGE_ROOT` のRSCペむロヌドがない堎合でも、島だけをハむドレヌトできたす。 + +Hydration Island は初回HTMLレンダリングのための戊略です。`"use hydrate"` が付いたコンポヌネントが埌からRSC曎新ペむロヌドの䞀郚ずしお描画される堎合は、新しい島を䜜らず通垞のコンポヌネント出力を返したす。そのサブツリヌはすでに既存のReactツリヌに所有されおいるためです。 + + +## Islands ず PPR の違い + + +Hydration Islands ず [Partial Pre-Rendering](/features/ppr) は、レンダリングパむプラむンの異なる郚分を扱いたす。 + +| 機胜 | 制埡するもの | 出力 | +|------|--------------|------| +| Partial Pre-Rendering | ルヌトセグメントをビルド時に描画するか、リク゚スト時に描画するか | 静的HTMLず、ストリヌミングされる動的なサヌバヌコンテンツ | +| Hydration Islands | サヌバヌレンダリングされたサブツリヌに、独立した遅延ハむドレヌション境界を持たせるか | 静的HTMLず、島専甚のRSCペむロヌドおよびロヌカルアりトレット | + +PPR は、ほずんどが静的なペヌゞシェルの䞭にリク゚スト時のサヌバヌコンテンツを入れたい堎合に䜿いたす。Hydration Islands は、サヌバヌレンダリング枈みHTMLをペヌゞルヌトをハむドレヌトせずに埌からむンタラクティブにしたい堎合に䜿いたす。この2぀は組み合わせるこずもできたす。PPRペヌゞの䞭に島を眮いおも、その島は独自のハむドレヌション戊略ずロヌカルアりトレットナビゲヌションを持ちたす。 + + +## Islandを䜜成する + + +コンポヌネント本䜓の䞭で `"use hydrate: "` を䜿いたす。このディレクティブは、むンラむンの `"use client"` や `"use server"` ず同じく字句スコヌプです。 + +```jsx +import { useState } from "react"; + +function Counter() { + "use client"; + + const [count, setCount] = useState(0); + return ; +} + +function CounterIsland() { + "use hydrate: idle; timeout=1200; id=counter"; + + return ; +} + +export default function App() { + return ( +
+

Server-rendered page

+ +
+ ); +} +``` + +䞊の䟋ではペヌゞルヌトはサヌバヌ専甚のたたです。島はすぐにサヌバヌレンダリング枈みHTMLを受け取り、指定した戊略に埓っおハむドレヌトされたす。 + + +## 戊略 + + +ディレクティブではコロンで戊略を指定し、セミコロン区切りでパラメヌタを枡したす。 + +```jsx +"use hydrate: idle; timeout=1200; id=counter"; +``` + +サポヌトされおいる戊略: + +| Strategy | 動䜜 | +|----------|------| +| `load` | クラむアント゚ントリが実行されるずすぐにハむドレヌト | +| `idle` | `requestIdleCallback` でハむドレヌトし、利甚できない堎合は `setTimeout` にフォヌルバック | +| `visible` | 島のマヌカヌがビュヌポヌトに入るずハむドレヌト | +| `interaction` | ナヌザヌ操䜜むベントでハむドレヌト | +| `media` | メディアク゚リが䞀臎したずきにハむドレヌト | +| `never` | HTMLだけを描画し、ハむドレヌトしない | + + +### `load` + + +`load` は、クラむアント゚ントリが開始され、島のペむロヌドが利甚可胜になるずすぐにハむドレヌトしたす。戊略を省略した堎合のデフォルトでもあるため、`"use hydrate"` ず `"use hydrate: load"` はどちらも eager な島を衚したす。 + +ペヌゞルヌトは静的に保ち぀぀、ファヌストビュヌ内のUIをできるだけ早くむンタラクティブにしたい堎合に䜿いたす。 + +```jsx +function SearchIsland() { + "use hydrate: load; id=search"; + + return ; +} +``` + +`load` でも島は独立したRSCペむロヌドずロヌカルアりトレットを䜿いたす。違いは、idle、viewport、ナヌザヌ操䜜、メディアク゚リを埅たずに、ブラりザ起動時にハむドレヌションを始める点です。 + + +### `idle` + + +`idle` は `requestIdleCallback` によっおブラりザのアむドル時間を埅っおからハむドレヌトしたす。`requestIdleCallback` が利甚できない環境では `setTimeout` にフォヌルバックしたす。任意の `timeout` パラメヌタで最倧埅機時間をミリ秒で指定でき、デフォルトは `2000` です。 + +初回衚瀺に必須ではないものの、読み蟌み埌しばらくしお䜿えるずよいUIに向いおいたす。たずえば二次的なカりンタヌ、䞋郚にあるフィルタヌ、蚭定パネル、優先床の䜎いりィゞェットなどです。 + +```jsx +function FiltersIsland() { + "use hydrate: idle; timeout=1200; id=filters"; + + return ; +} +``` + +idle の島もHTMLはすぐにサヌバヌレンダリングされたす。ただし idle タスクが実行されハむドレヌションが完了するたではむンタラクティブではありたせん。ナヌザヌがすぐクリックしそうなコントロヌルには避けおください。 + + +### `visible` + + +`visible` は `IntersectionObserver` を䜿い、島のラッパヌがビュヌポヌトず亀差したずきにハむドレヌトしたす。`rootMargin` で実際に芋える前からハむドレヌションを開始でき、`threshold` で必芁な可芖割合を指定できたす。デフォルトの `rootMargin` は `600px`、デフォルトの `threshold` は `0` です。 + +長いペヌゞやファヌストビュヌ倖のコンテンツに向いおいたす。deferred な島だけで䜿われるクラむアントコンポヌネントは初期の modulepreload から倖れ、島がビュヌポヌトに近づいたずきに読み蟌たれおハむドレヌトされたす。 + +```jsx +function VisibleIsland() { + "use hydrate: visible; rootMargin=0px; threshold=0.2; id=visible_counter"; + + return ; +} +``` + +`IntersectionObserver` が利甚できない堎合、UIを氞遠に非むンタラクティブなたたにしないため、ランタむムは即座にハむドレヌトしたす。 + + +### `interaction` + + +`interaction` は、ナヌザヌが島のラッパヌに察しお操䜜したずきにハむドレヌトしたす。デフォルトでは `pointerenter`、`focusin`、`pointerdown`、`click` を監芖したす。`events` パラメヌタを枡すず、カンマ区切りのDOMむベント䞀芧で眮き換えられたす。 + +ナヌザヌの意図が芋えるたで静的なたたでよいUIに向いおいたす。メニュヌ、ポップオヌバヌ、展開パネル、評䟡コントロヌル、毎回䜿われるずは限らない重いコントロヌルなどです。 + +```jsx +function MenuIsland() { + "use hydrate: interaction; events=pointerenter,focusin; id=account_menu"; + + return ; +} +``` + +最初に䞀臎したむベントはハむドレヌションを開始したす。その同じむベントが、ハむドレヌト埌のコンポヌネントに再送されるこずを前提にしないでください。最初のクリックでコンポヌネント偎の `onClick` を必ず実行したい堎合は、`pointerenter` や `focusin` のような早い intent むベントを䜿うか、`load` を䜿っおください。 + + +### `media` + + +`media` は `matchMedia` のク゚リが䞀臎したずきにハむドレヌトしたす。ク゚リは `query` パラメヌタで指定したす。クラむアント゚ントリ実行時にすでに䞀臎しおいれば即座にハむドレヌトし、䞀臎しおいなければメディアク゚リの倉曎を埅ちたす。 + +特定の環境でだけクラむアント動䜜が必芁なレスポンシブUIに向いおいたす。デスクトップ専甚ツヌルバヌ、ワむド画面向けむンスペクタヌ、reduced motion向けの代替UI、ポむンタヌ操䜜可胜なレむアりトだけで必芁なコントロヌルなどです。 + +```jsx +function DesktopIsland() { + "use hydrate: media; query=(min-width: 900px); id=desktop_tools"; + + return ; +} +``` + +モヌション蚭定も `media` で扱いたす。`prefers-reduced-motion` は暙準のメディアク゚リだからです。たずえば、ナヌザヌが reduced motion を芁求しおいない堎合だけ、アニメヌションの重いりィゞェットをハむドレヌトできたす。 + +```jsx +function MotionIsland() { + "use hydrate: media; query=(prefers-reduced-motion: no-preference); id=motion"; + + return ; +} +``` + +逆に、reduced motion を芁求しおいるナヌザヌ向けのコントロヌルだけをハむドレヌトするこずもできたす。 + +```jsx +function ReducedMotionIsland() { + "use hydrate: media; query=(prefers-reduced-motion: reduce); id=reduced_motion"; + + return ; +} +``` + +`matchMedia` が利甚できない堎合、たたは `query` が指定されおいない堎合、ランタむムは即座にハむドレヌトしたす。 + + +### `never` + + +`never` は島のHTMLを描画したすが、ハむドレヌション甚ペむロヌドを䜜成せず、島をハむドレヌトしたせん。結果は静的なサヌバヌレンダリング枈みマヌクアップです。 + +同じコンポヌネント圢状を戊略比范の䞭で䜿いたい堎合や、絶察にむンタラクティブにしない静的コンテンツずしお明瀺したい堎合に䜿いたす。リク゚ストスコヌプのハむドレヌションデヌタなしで島マヌクアップを出力できるこずを確認するテスト fixture ずしおも䟿利です。 + +```jsx +function StaticPromoIsland() { + "use hydrate: never; id=static_promo"; + + return ; +} +``` + +ハむドレヌション甚ペむロヌドが存圚しないため、`never` の䞭のクラむアントコンポヌネントはサヌバヌレンダリング枈みHTMLずしおだけ衚珟されたす。むベントハンドラや effect は実行されたせん。 + + +## ロヌカルアりトレットナビゲヌション + + +Hydration Island はロヌカルの非ルヌトアりトレットずしおハむドレヌトされたす。島の䞭では `Link local` ず `Refresh local` を䜿い、ペヌゞルヌトをハむドレヌトたたはナビゲヌトせずに、その島だけを曎新できたす。 + +```jsx +import { useSearchParams, useUrl } from "@lazarv/react-server"; +import { Link, Refresh } from "@lazarv/react-server/navigation"; + +function NavigationIsland() { + "use hydrate: load; id=rsc_navigation"; + + const url = useUrl(); + const search = useSearchParams(); + const view = search?.view === "details" ? "details" : "overview"; + + return ( +
+

{url.pathname}{url.search}

+

{view}

+ Overview + Details + Refresh island +
+ ); +} +``` + +`Link local` は島の `@.rsc.x-component` ペむロヌドを取埗し、そのアりトレットだけを差し替えたす。明瀺的にルヌトを察象にしない限り、ブラりザURLず `PAGE_ROOT` のハむドレヌション状態は倉曎されたせん。 + +Hydration Islands は、ハむドレヌトされた埌にだけクラむアントナビゲヌションぞ参加したす。ただハむドレヌトされおいない島はクラむアントアりトレットずしお登録されおいないため、`Link`、`Refresh`、`pushstate`、`replacestate`、`popstate` の曎新を受け取りたせん。埌から島がハむドレヌトされるず、サヌバヌレンダリング枈みの島ペむロヌドず珟圚のブラりザロケヌションから開始したす。 + +島のナビゲヌションで明瀺的に `push` や `replace` を䜿っおブラりザ履歎ぞ参加した堎合でも、ブラりザの戻る/進む操䜜はグロヌバルなブラりザむベントです。ハむドレヌト枈みのアりトレットは、それらのむベントに同時に反応できたす。過去のブラりザ履歎むベントの埌にハむドレヌトされた島は、その取り逃したむベントを再生したせん。 + + +## DevTools + + +DevTools を有効にするず、Hydration Islands は Outlets パネルに `island` バッゞ付きで衚瀺されたす。パネルには、島がすでにハむドレヌト枈みかどうかも衚瀺されたす。島はロヌカルアりトレットでありリモヌトコンポヌネントではないため、Remotes パネルには衚瀺されたせん。 + + +## Example + + +リポゞトリには完党なサンプルが含たれおいたす。 + +```sh +pnpm --filter ./examples/hydration-islands dev --open +``` diff --git a/docs/src/pages/ja/features.(index).mdx b/docs/src/pages/ja/features.(index).mdx index 7afacb50..c74fd28c 100644 --- a/docs/src/pages/ja/features.(index).mdx +++ b/docs/src/pages/ja/features.(index).mdx @@ -14,7 +14,7 @@ ゚ラヌ凊理に぀いおは、組み蟌みの[゚ラヌバりンダリ](/features/error-handling)コンポヌネントの䜿甚方法や、独自の゚ラヌ凊理を実装する方法に぀いお孊ぶこずができたす。 -たた、[郚分的プリレンダリング](/features/partial-rendering)、[クラスタヌモヌド](/features/cluster)、[ミドルりェアモヌド](/features/middleware-mode)など、䟿利な機胜に぀いおも孊ぶこずができたす。郚分的なプリレンダリングは、アプリの䞀郚だけをプリレンダリングしたい堎合に䟿利です。クラスタヌモヌドは、マルチプロセス環境でアプリを実行したい堎合に圹立ちたす。䞀方、ミドルりェアモヌドは、ExpressやNestJSなどの既存のサヌバヌでアプリをミドルりェアずしお実行したい堎合に䟿利です。 +たた、[郚分的プリレンダリング](/features/ppr)、[Hydration Islands](/features/hydration-islands)、[クラスタヌモヌド](/features/cluster)、[ミドルりェアモヌド](/features/middleware-mode)など、䟿利な機胜に぀いおも孊ぶこずができたす。郚分的なプリレンダリングは、アプリの䞀郚だけをプリレンダリングしたい堎合に䟿利です。Hydration Islands は、サヌバヌレンダリングされたルヌトを静的なたたにし、遞択したサブツリヌだけをロヌカルアりトレットずしおハむドレヌトしたい堎合に䟿利です。クラスタヌモヌドは、マルチプロセス環境でアプリを実行したい堎合に圹立ちたす。䞀方、ミドルりェアモヌドは、ExpressやNestJSなどの既存のサヌバヌでアプリをミドルりェアずしお実行したい堎合に䟿利です。 [マむクロフロント゚ンド](/features/micro-frontends)セクションでは、マむクロフロント゚ンドアヌキテクチャを実装する方法に぀いお孊ぶこずができたす。ランタむムはアプリでマむクロフロント゚ンドを実装するための䞀連のツヌルを提䟛しおいたす。`RemoteComponent`コンポヌネントを䜿甚しお、リモヌトURLからマむクロフロント゚ンドを読み蟌み、サヌバヌサむドレンダリングを䜿甚しおアプリ内でレンダリングできたす。サヌバヌサむドレンダリングがサポヌトする`iframe`フラグメントをReactアプリケヌションで䜿甚できたす diff --git a/examples/hydration-islands/App.jsx b/examples/hydration-islands/App.jsx new file mode 100644 index 00000000..7e46ab15 --- /dev/null +++ b/examples/hydration-islands/App.jsx @@ -0,0 +1,442 @@ +import { useUrl, useSearchParams } from "@lazarv/react-server"; +import { Link, Refresh } from "@lazarv/react-server/navigation"; +import Counter from "./Counter.jsx"; +import RootClient from "./RootClient.jsx"; +import StrategyProbe from "./StrategyProbe.jsx"; +import VisibilityLoadedClient from "./VisibilityLoadedClient.jsx"; + +function CounterIsland() { + "use hydrate: idle; timeout=1200; id=counter"; + + return ; +} + +function VisibleCounterIsland() { + "use hydrate: visible; rootMargin=0px; threshold=0.2; id=visible_counter"; + + return ; +} + +function VisibleClientLoadIsland() { + "use hydrate: visible; rootMargin=0px; threshold=0.2; id=visible_client_loader"; + + return ; +} + +function MixedCounterIsland() { + "use hydrate: load; id=mixed_counter"; + + return ; +} + +function MixedVisibleClientLoadIsland() { + "use hydrate: visible; rootMargin=0px; threshold=0.2; id=mixed_visible_client_loader"; + + return ; +} + +function LateCounterIsland() { + "use hydrate: load; id=late_counter"; + + return ; +} + +function RscNavigationIsland() { + "use hydrate: load; id=rsc_navigation"; + + const url = useUrl(); + const search = useSearchParams(); + const rawView = Array.isArray(search?.view) ? search.view[0] : search?.view; + const view = rawView === "details" ? "details" : "overview"; + const outletUrl = `${url.pathname}${url.search}`; + const renderId = Date.now().toString(36); + + return ( +
+
+

RSC island

+

Outlet navigation

+

+ {view === "details" + ? "Details view rendered by the island outlet." + : "Overview view rendered by the island outlet."} +

+

+ {outletUrl} +

+

+ {renderId} +

+
+ +
+ ); +} + +function LoadStrategyIsland() { + "use hydrate: load; id=strategy_load"; + + return ; +} + +function IdleStrategyIsland() { + "use hydrate: idle; timeout=50; id=strategy_idle"; + + return ; +} + +function VisibleStrategyIsland() { + "use hydrate: visible; rootMargin=0px; threshold=0.2; id=strategy_visible"; + + return ; +} + +function InteractionStrategyIsland() { + "use hydrate: interaction; events=pointerenter; id=strategy_interaction"; + + return ( + + ); +} + +function MediaStrategyIsland() { + "use hydrate: media; query=(min-width: 700px); id=strategy_media"; + + return ; +} + +function NeverStrategyIsland() { + "use hydrate: never; id=strategy_never"; + + return ; +} + +function searchValue(search, key) { + const value = search?.[key]; + return Array.isArray(value) ? value[0] : value; +} + +export default function App() { + const search = useSearchParams(); + const mode = searchValue(search, "mode") || "rootless"; + const mixed = mode === "mixed"; + const lateEmpty = mode === "late-empty"; + const lateIsland = mode === "late-island"; + const strategies = mode === "strategies"; + + return ( + + + Hydration Islands + + + + +
+
+

Static root, hydrated island

+

+ The page root has no client component. The counter below is + rendered into HTML and hydrated later as a non-root outlet. +

+
+
+

Server-only root

+

+ {mixed || lateEmpty || lateIsland + ? "This page also hydrates the page root. The island below still hydrates as a local outlet." + : strategies + ? "This strategy fixture keeps the page root static while each island chooses its own hydration trigger." + : "This section never hydrates. Only the component marked with "} + {mixed || lateEmpty || lateIsland || strategies ? null : ( + use hydrate + )} + {mixed || lateEmpty || lateIsland || strategies + ? null + : " receives client interactivity."} +

+
+ {strategies ? ( +
+
+

Strategy matrix

+

All hydration strategies

+

+ This fixture keeps one island for each strategy on the same + page so tests can exercise scheduling and hydration behavior + together. +

+
+
+ + + + + +
+ +
+
+
+ ) : mixed ? ( + <> + + +
+
+

Mixed viewport client module

+

+ This island runs inside a hydrated page root, but its client + component still loads only after it becomes visible. +

+ +
+
+ + ) : lateEmpty || lateIsland ? ( + <> + +
+
+

RSC navigation

+

Island introduced by navigation

+

+ Start without the component, then fetch a new RSC payload. + In an RSC update, the use hydrate boundary renders as + regular React content instead of a new island. +

+
+ +
+ {lateIsland ? : null} + + ) : ( + <> + + +
+
+

Viewport strategy

+

+ This island hydrates when its marker intersects the + viewport. +

+ +
+
+
+
+

Viewport client module

+

+ This island keeps its client component module out of the + browser until the visibility strategy starts hydration. +

+ +
+
+ + )} +
+ + + ); +} diff --git a/examples/hydration-islands/Counter.jsx b/examples/hydration-islands/Counter.jsx new file mode 100644 index 00000000..17391320 --- /dev/null +++ b/examples/hydration-islands/Counter.jsx @@ -0,0 +1,29 @@ +"use client"; + +import { useEffect, useState } from "react"; + +export default function Counter({ initial = 0, title = "Counter island" }) { + const [count, setCount] = useState(initial); + const [hydrated, setHydrated] = useState(false); + + useEffect(() => { + setHydrated(true); + }, []); + + return ( +
+
+

Hydration island

+

{title}

+

+ {hydrated + ? "This subtree is now hydrated as its own outlet." + : "This HTML was rendered on the server and is not interactive yet."} +

+
+ +
+ ); +} diff --git a/examples/hydration-islands/RootClient.jsx b/examples/hydration-islands/RootClient.jsx new file mode 100644 index 00000000..18d357a8 --- /dev/null +++ b/examples/hydration-islands/RootClient.jsx @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect, useState } from "react"; + +export default function RootClient() { + const [count, setCount] = useState(0); + const [hydrated, setHydrated] = useState(false); + + useEffect(() => { + setHydrated(true); + }, []); + + return ( +
+
+

Page root

+

Client component above the island

+

+ {hydrated + ? "The page root is hydrated." + : "The page root is server-rendered."} +

+
+ +
+ ); +} diff --git a/examples/hydration-islands/StrategyProbe.jsx b/examples/hydration-islands/StrategyProbe.jsx new file mode 100644 index 00000000..8cb1ca54 --- /dev/null +++ b/examples/hydration-islands/StrategyProbe.jsx @@ -0,0 +1,34 @@ +"use client"; + +import { useEffect, useState } from "react"; + +export default function StrategyProbe({ name, initial, title }) { + const [count, setCount] = useState(initial); + const [hydrated, setHydrated] = useState(false); + + useEffect(() => { + setHydrated(true); + }, []); + + return ( +
+
+

Strategy probe

+

{title}

+

+ {hydrated ? `${title} hydrated.` : `${title} server HTML.`} +

+
+ +
+ ); +} diff --git a/examples/hydration-islands/VisibilityLoadedClient.jsx b/examples/hydration-islands/VisibilityLoadedClient.jsx new file mode 100644 index 00000000..28786bf9 --- /dev/null +++ b/examples/hydration-islands/VisibilityLoadedClient.jsx @@ -0,0 +1,35 @@ +"use client"; + +import { useEffect, useState } from "react"; + +if (typeof window !== "undefined") { + window.__react_server_visible_client_loader_module_loads__ = + (window.__react_server_visible_client_loader_module_loads__ ?? 0) + 1; +} + +export default function VisibilityLoadedClient() { + const [enabled, setEnabled] = useState(false); + const [hydrated, setHydrated] = useState(false); + + useEffect(() => { + window.__react_server_visible_client_loader_hydrated__ = true; + setHydrated(true); + }, []); + + return ( +
+
+

Visibility client

+

Client loaded on hydration

+

+ {hydrated + ? "The client module loaded after the island became visible." + : "This client component is server-rendered HTML for now."} +

+
+ +
+ ); +} diff --git a/examples/hydration-islands/package.json b/examples/hydration-islands/package.json new file mode 100644 index 00000000..fbf92ddd --- /dev/null +++ b/examples/hydration-islands/package.json @@ -0,0 +1,13 @@ +{ + "name": "@lazarv/react-server-example-hydration-islands", + "private": true, + "description": "@lazarv/react-server hydration islands example", + "scripts": { + "dev": "react-server ./App.jsx", + "build": "react-server build ./App.jsx", + "start": "react-server start" + }, + "dependencies": { + "@lazarv/react-server": "workspace:^" + } +} diff --git a/packages/react-server/README.md b/packages/react-server/README.md index 8979a6eb..0e35b86e 100644 --- a/packages/react-server/README.md +++ b/packages/react-server/README.md @@ -54,6 +54,7 @@ Built on Vite for instant HMR. Ships with its own React, so your project stays l | **Server Functions** | `"use server"` with progressive enhancement and form actions | | **File-System Router** | Pages, layouts, outlets, API routes, middlewares, error boundaries, loading states | | **Client Navigation** | SPA-like navigation with prefetching, rollback, and outlet-scoped updates | +| **Hydration Islands** | `"use hydrate"` for server-rendered subtrees that hydrate later as local outlets | | **Caching** | Response cache, in-memory cache, `"use cache"` directive, Unstorage providers | | **Static Export** | Full static generation with dynamic route params and compression | | **Partial Pre-Rendering** | `"use dynamic"` / `"use static"` for mixed static + runtime rendering | @@ -94,6 +95,20 @@ export default function Counter() { } ``` +## Hydration Islands + +Use `"use hydrate"` inside a server component function to render that subtree as HTML immediately and hydrate it later as a local non-root outlet. The page root can remain server-only with no `PAGE_ROOT` RSC payload. During later RSC update payloads, the same component renders as normal React content instead of creating a new island. + +```jsx +function CounterIsland() { + "use hydrate: visible; rootMargin=0px; threshold=0.2; id=counter"; + + return ; +} +``` + +Strategies include `load`, `idle`, `visible`, `interaction`, `media`, and `never`. Inside a hydrated island, `Link local` and `Refresh local` update the island outlet without hydrating or navigating the page root. + ## Server Functions `"use server"` turns any async function into a server-side RPC endpoint. Works as form actions with progressive enhancement — no JavaScript required for basic form submissions. @@ -337,6 +352,7 @@ cd react-server && pnpm install | Bun | `pnpm --filter ./examples/bun dev` | | Deno | `pnpm --filter ./examples/deno dev` | | Partial Pre-Rendering | `pnpm --filter ./examples/ppr dev --open` | +| Hydration Islands | `pnpm --filter ./examples/hydration-islands dev --open` | | Micro-Frontends | `pnpm --filter ./examples/remote dev` | | MCP Server | `pnpm --filter ./examples/mcp dev` | | Workers | `pnpm --filter ./examples/use-worker dev --open` | @@ -356,4 +372,4 @@ Contributions are welcome! Check out the [contributing guide](https://github.com ## License -[MIT](https://github.com/lazarv/react-server/blob/main/LICENSE) \ No newline at end of file +[MIT](https://github.com/lazarv/react-server/blob/main/LICENSE) diff --git a/packages/react-server/client/ClientProvider.jsx b/packages/react-server/client/ClientProvider.jsx index 9e135a12..116c0c3d 100644 --- a/packages/react-server/client/ClientProvider.jsx +++ b/packages/react-server/client/ClientProvider.jsx @@ -48,10 +48,25 @@ const liveOutlets = new Set(); let liveRegistryLoader = null; const outletTemporaryReferences = new Map(); +function getHydrationIslandState(outlet) { + if (typeof self === "undefined") return undefined; + const islands = self.__react_server_hydration_islands__ || {}; + const states = self.__react_server_hydration_island_states__ || {}; + for (const [id, data] of Object.entries(islands)) { + if ((data?.outlet || id) === outlet) { + return states[id] || "pending"; + } + } + return undefined; +} + if (import.meta.env.DEV) { window.__react_server_devtools_outlets__ = () => Array.from(outlets.entries()).map(([name, url]) => { const meta = outletMeta.get(name) || {}; + const hydrationState = meta.island + ? getHydrationIslandState(name) + : undefined; return { name, url: typeof url === "string" ? url : (url?.toString?.() ?? ""), @@ -60,6 +75,9 @@ if (import.meta.env.DEV) { defer: meta.defer || false, isolate: meta.isolate || false, ttl: meta.ttl ?? null, + island: meta.island || false, + hydrationState, + hydrated: hydrationState === "hydrated", }; }); window.__react_server_devtools_routes__ = getAllRoutes; @@ -139,7 +157,8 @@ const registerOutlet = ( defer, live = false, isolate = false, - ttl + ttl, + island = false ) => { outlets.set(outlet, url); outletMeta.set(outlet, { @@ -148,6 +167,7 @@ const registerOutlet = ( live: !!live, isolate: !!isolate, ttl: ttl ?? null, + island: !!island, }); if (live) { liveOutlets.add(outlet); diff --git a/packages/react-server/client/HydrationIslandBoundary.jsx b/packages/react-server/client/HydrationIslandBoundary.jsx new file mode 100644 index 00000000..efae2597 --- /dev/null +++ b/packages/react-server/client/HydrationIslandBoundary.jsx @@ -0,0 +1,164 @@ +"use client"; + +import React, { + Suspense, + use, + useEffect, + useMemo, + useRef, + useState, +} from "react"; + +import ReactServerComponent from "./ReactServerComponent.jsx"; +import { getHydrationIslandContent } from "./hydration-island-data.mjs"; +import { + flightStreamFromPayload, + runHydrationStrategy, + shouldDeferHydrationStrategy, +} from "./hydration-island-runtime.mjs"; +import { activateScriptTemplates } from "./script-templates.mjs"; + +const resources = new Map(); + +function getResource(id) { + let resource = resources.get(id); + if (!resource) { + let resolvePromise; + const promise = new Promise((resolve) => { + resolvePromise = resolve; + }); + resource = { + promise, + resolved: false, + started: false, + resolve() { + if (resource.resolved) return; + resource.resolved = true; + resolvePromise(); + }, + start(element, strategy) { + if (resource.started || resource.resolved) return; + resource.started = true; + runHydrationStrategy(element, strategy, resource.resolve); + }, + }; + resources.set(id, resource); + } + return resource; +} + +function HydrationIslandCommitEffect({ id }) { + useEffect(() => { + const markHydrated = () => { + const states = (self.__react_server_hydration_island_states__ ??= {}); + states[id] = "hydrated"; + }; + if (typeof requestAnimationFrame === "function") { + const frame = requestAnimationFrame(markHydrated); + return () => cancelAnimationFrame(frame); + } + const timeout = setTimeout(markHydrated, 0); + return () => clearTimeout(timeout); + }, [id]); + + return null; +} + +function HydrationIslandOutlet({ id, outlet, url, strategy, resource }) { + if (shouldDeferHydrationStrategy(strategy)) { + use(resource.promise); + } + + return ( + <> + + + + ); +} + +function readHydrationIslandData(cacheKey) { + const data = getHydrationIslandContent(cacheKey); + return data && typeof data.then === "function" ? use(data) : data; +} + +function HydrationIslandContent({ data }) { + return data?.content ?? null; +} + +export default function HydrationIslandBoundary({ + id, + outlet = id, + url, + strategy = { type: "load" }, + cacheKey, +}) { + const elementRef = useRef(null); + const [active, setActive] = useState(false); + const resource = useMemo(() => getResource(id), [id]); + const hydrationData = readHydrationIslandData(cacheKey); + + useEffect(() => { + const data = { + id, + outlet, + url, + strategy, + }; + const states = (self.__react_server_hydration_island_states__ ??= {}); + (self.__react_server_hydration_islands__ ??= {})[id] = data; + + if (strategy?.type === "never") { + return; + } + + activateScriptTemplates(document); + self.__react_server_hydrate_islands__?.(); + + if (hydrationData?.payload && !self[`__flightStream__${outlet}__`]) { + self[`__flightStream__${outlet}__`] = flightStreamFromPayload( + hydrationData.payload + ); + } + + if (!self[`__flightStream__${outlet}__`]) { + return; + } + + self[`__flightHydration__${outlet}__`] = false; + + if (shouldDeferHydrationStrategy(strategy)) { + states[id] = states[id] || "scheduled"; + resource.start(elementRef.current, strategy); + } else { + states[id] = "hydrating"; + resource.resolve(); + } + + setActive(true); + }, [id, outlet, url, strategy, resource, hydrationData]); + + return ( +
+ {active ? ( + }> + + + ) : ( + + )} +
+ ); +} diff --git a/packages/react-server/client/ReactServerComponent.jsx b/packages/react-server/client/ReactServerComponent.jsx index 220d507a..bd785ca2 100644 --- a/packages/react-server/client/ReactServerComponent.jsx +++ b/packages/react-server/client/ReactServerComponent.jsx @@ -24,6 +24,7 @@ import { emitLocationChange, clearPendingNavigation, } from "./client-location.mjs"; +import { activateScriptTemplates } from "./script-templates.mjs"; // Client-root SSR shortcut: when ssr-handler.mjs took the render-ssr.jsx // path, the HTML carries `self.__react_server_root__ = "id#name"` (string, @@ -45,26 +46,6 @@ function initialClientRootComponent({ outlet, remote }) { return React.createElement(Component); } -// Execute scripts stored as