Navigation
Segment rendering. Only the changed portion fetches and swaps. No full reloads.
Link
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>| Prop | Type | Default | Description |
|---|---|---|---|
href | string | — | Target URL |
replace | boolean | false | replaceState vs pushState |
scroll | boolean | true | Scroll to top after navigation |
prefetch | boolean | false | Fetch the target page on hover and focus (see below) |
Renders as <a> with click handler. Works without JavaScript (falls back to normal link).
Prefetching
<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.
navigate()
Programmatic navigation:
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:
// 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:
import { useNavigationState } from '@nestjs-ssr/react/client';
function Nav() {
const state = useNavigationState(); // 'idle' | 'loading'
return state === 'loading' ? <Spinner /> : null;
}How It Works
- Click
Linkor callnavigate() - Read
data-layoutattributes from DOM - Fetch with
X-Current-Layoutsheader - Server returns
{ html, swapTarget, props } - Swap content in matching
data-outlet - Hydrate new segment
- Update history and head
Layout Markers
Server renders with data attributes:
<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 projectinitcreates), a component default-exported from a file in aviewsdirectory keeps its name automatically, whatever the file is called. Nothing to add. - Without the plugin, or for a layout that lives outside a
viewsdirectory, setdisplayName:
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:
::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-segmentThe 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.