Skip to content

Navigation ​

Segment rendering. Only the changed portion fetches and swaps. No full reloads.

tsx
import { Link } from '@nestjs-ssr/react/client';

<Link href="/products">Products</Link>
<Link href="/about" replace>About</Link>
<Link href="/settings" scroll={false}>Settings</Link>
PropTypeDefaultDescription
hrefstring—Target URL
replacebooleanfalsereplaceState vs pushState
scrollbooleantrueScroll to top after navigation
prefetchbooleanfalseFetch the target page on hover and focus (see below)

Renders as <a> with click handler. Works without JavaScript (falls back to normal link).

Prefetching ​

tsx
<Link href="/recipes" prefetch>Recipes</Link>

With prefetch, hovering or focusing the link fetches the target page's segment, then its code (with per-route view loading). A click within ten seconds uses that response instead of fetching again. It is off by default because it sends a request a user who never clicks does not need; turn it on for the links people are likely to follow. prefetch(url) does the same from code.

Programmatic navigation:

typescript
import { navigate } from '@nestjs-ssr/react/client';

await navigate('/dashboard');
await navigate('/settings', { replace: true, scroll: false });

Returns a Promise. Resolves after hydration completes.

Same-origin only ​

navigate() accepts same-origin http(s) URLs. Anything else — another origin, a protocol-relative //host/path, javascript:, data: — is refused with a console error and no request. Link behaves the same way, but hands the click back to the browser instead, so external links keep working as ordinary links.

This matters when the URL comes from user input:

typescript
// Unsafe input is rejected, not followed.
await navigate(new URLSearchParams(location.search).get('next') ?? '/');

A segment response is parsed as JSON and its html written into the DOM, so a third-party server answering with permissive CORS headers would otherwise be able to inject markup into your origin. To leave the app, assign to window.location yourself.

useNavigationState ​

Loading state for spinners:

tsx
import { useNavigationState } from '@nestjs-ssr/react/client';

function Nav() {
  const state = useNavigationState(); // 'idle' | 'loading'
  return state === 'loading' ? <Spinner /> : null;
}

How It Works ​

  1. Click Link or call navigate()
  2. Read data-layout attributes from DOM
  3. Fetch with X-Current-Layouts header
  4. Server returns { html, swapTarget, props }
  5. Swap content in matching data-outlet
  6. Hydrate new segment
  7. Update history and head

Layout Markers ​

Server renders with data attributes:

html
<div data-layout="RootLayout">
  <div data-outlet="RootLayout">
    <div data-layout="AdminLayout">
      <div data-outlet="AdminLayout">
        <!-- page content -->
      </div>
    </div>
  </div>
</div>

Navigation swaps the smallest changed segment.

Component names ​

The client finds the page and layouts to hydrate by the component names the server sends. Production builds minify function names, so they have to survive the build:

  • With the nestjsSsr() Vite plugin (in every project init creates), a component default-exported from a file in a views directory keeps its name automatically, whatever the file is called. Nothing to add.
  • Without the plugin, or for a layout that lives outside a views directory, set displayName:
tsx
export default function RootLayout({ children }: LayoutProps) {
  return <div>{children}</div>;
}

RootLayout.displayName = 'RootLayout';

Without either, names become default or e in production and layout detection breaks.

View Transitions ​

Uses View Transitions API when available:

css
::view-transition-old(root) {
  animation: fade-out 150ms ease-out;
}

::view-transition-new(root) {
  animation: fade-in 150ms ease-in;
}

Falls back to instant swap.

Fallbacks ​

Full page navigation when:

  • No layouts in DOM
  • No common ancestor layout
  • Fetch fails
  • The segment response fails validation (see below)
  • JavaScript disabled

Always works. Progressive enhancement.

Segment responses ​

A segment response is served as application/json and carries a schema version. Before any of it reaches the DOM, the client checks that:

  • the schema version is one it understands,
  • the body is within the segment size limit,
  • the swap target is a layout name that exists in the current layout tree,
  • the component name, props, head, context, and layout list have the expected shapes.

Anything that fails becomes a full navigation rather than a partially applied fragment. Unknown properties are dropped.

Segments are always derived from a route's HTML representation, so a JSON API DTO is never served as a navigation payload.

Trusted Types ​

All segment markup is written to the DOM in one place, through a Trusted Types policy named nestjs-ssr-segment. If your Content-Security-Policy enforces Trusted Types, allow that policy name:

Content-Security-Policy: require-trusted-types-for 'script'; trusted-types nestjs-ssr-segment

The policy vouches for markup that came from your own server; it does not sanitize. HTML your application injects itself — anything passed to dangerouslySetInnerHTML — still needs an application-side sanitizer.

Released under the MIT License.