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/package.json b/package.json index 7149f3e5..0642d18e 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "oxlint --fix" ] }, - "packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8", + "packageManager": "pnpm@10.33.4+sha512.1c67b3b359b2d408119ba1ed289f34b8fc3c6873412bec6fd264fbdc82489e510fcbecb9ce9d22dae7f3b76269d8441046014bdca53b9979cd7a561ad631b800", "pnpm": { "overrides": { "react-click-away-listener>react": "0.0.0-experimental-561ed529-20260423", 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