`:
```tsx
import { Suspense } from 'react';
import type { ReactNode } from 'react';
import { Link } from 'waku';
export default function Layout({ children }: { children: ReactNode }) {
return (
Loading...}>{children}
);
}
```
The page it streams in is an ordinary dynamic route:
```tsx
import type { PageProps } from 'waku/router';
export default async function Post({ id }: PageProps<'/posts/[id]'>) {
const post = await loadPost(id); // request-specific work
return {post.body};
}
export const getConfig = async () => {
return { render: 'dynamic' } as const;
};
```
You can also navigate programmatically with `router.push` or `router.replace`:
```tsx
'use client';
import { useRouter } from 'waku';
export const PostButton = () => {
const router = useRouter();
return (
);
};
```
Both return a promise that resolves once the navigation you asked for has been handled: after the response when the route needs one, right away when it does not, such as a static route already loaded once or the route you are already on, and also when a newer navigation supersedes it.
When a fetch redirects to another route or needs the custom 404 page, the same navigation fetches that destination before it resolves. Anything your page throws later while rendering, such as a late `unstable_notFound()` or `unstable_redirect()`, is followed by a navigation of its own after this promise has settled.
A missing route depends on who answers the request. A Waku server can send the 404 page as the first response. Where the first request instead reports a missing route, the client fetches the custom 404 page itself. In both cases, the promise resolves with the `/404` route while the address bar keeps the URL that was requested. Without a custom 404 route, the promise rejects and the built-in Not Found page is shown.
The promise also rejects when the navigation fails outright and when a redirect hands the page to the browser. A redirect to a route of this app is different: the client follows it and the promise resolves with that destination. Catch failures when you call these methods programmatically. Navigation failures are rendered through `ErrorBoundary`. The promise does not wait for React to finish rendering the destination, so the address bar may still show the previous URL when it resolves.
When the shell is cached, the layout and the `Loading...` fallback appear with no round trip, and the post's content streams into the fallback once the server responds. When the shell is **not** cached, the navigation falls back to normal behavior: the current page stays put until the response arrives, so there is no blank flash.
The `` boundary decides how localized the loading state is. Wrapping the whole page area (as above) swaps the page for a skeleton; wrapping only a dynamic slice inside an otherwise-static page leaves the rest in place and shows the skeleton for just that slice.
Because an instant navigation commits before the server responds, it reconciles afterward: a server redirect updates the URL to the redirect target, while a not-found (404) response keeps the requested URL, so the 404 page shows at the address the user tried.
An instant navigation that can commit from cache, or that needs no fetch, commits urgently rather than inside a React transition, since a transition would suppress the immediate skeleton. A `` therefore does not animate that commit. When the shell is not cached, navigation waits for the response and commits the destination in a transition; `useNavigationStatus_UNSTABLE()` reports `pending` for that wait. When the cache commit is available, the shell itself is the pending state.
When an instant navigation paints a cached shell, the dynamic slots the client already had lose their etags, including slots from earlier pages and slots that opted in with [`unstable_getEtag`](/guides/explicit-caching#etags-skipping-unchanged-payloads). The next regular navigation or server action that includes such a slot has the server render and send it again, and the slot gets its etag back. Prefetches are not affected, because they send the etags of the prefetched response. Instant navigation suits dynamic parts that change often or are cheap to render. If your app relies on etags to skip costly dynamic slots, pair them with `cache.rsc` so the server replays the cached subtree instead of rendering it again, or do not use `unstable_instant`.
## Pending UI
A descendant of `` can read the navigation status with `useNavigationStatus_UNSTABLE()` and render pending UI while a navigation transition is in progress. It works like React's `useFormStatus`: it reflects the nearest enclosing ``, and `pending` stays `true` until the destination route's async components resolve (including client-only Suspense).
```tsx
'use client';
import { useNavigationStatus_UNSTABLE as useNavigationStatus } from 'waku/router/client';
export const PendingIndicator = () => {
const { pending } = useNavigationStatus();
return (
Loading...
);
};
```
```tsx
'use client';
import { Link as WakuLink } from 'waku';
import type { LinkProps } from 'waku/router/client';
import { PendingIndicator } from './pending-indicator';
export const PendingLink = ({ children, ...props }: LinkProps) => (
{children}
);
```
Use `PendingLink` where a link needs an indicator. Keep the indicator non-interactive and `aria-hidden` (as above) so it does not affect the link's accessible name or introduce a nested interactive target.
`useNavigationStatus_UNSTABLE()` must be called from a Client Component rendered inside the `` because the status belongs to the nearest link. It does not observe programmatic navigation, browser back and forward, or a link with a custom transition. Instant links report `pending` when they fall back to waiting for a response; a cache commit does not, because the shell itself is the pending state. Called outside any ``, the hook returns an empty object (not `{ pending: false }`).
## Observe Route Changes
`useRouter().unstable_events` was removed. For pending UI on a link, use `useNavigationStatus_UNSTABLE()` above. For view tracking, observe the committed route fields:
```tsx
'use client';
import { useEffect } from 'react';
import { useRouter } from 'waku';
export const NavigationLogger = () => {
const { path, query, hash } = useRouter();
useEffect(() => {
console.log('viewed route', { path, query, hash });
}, [path, query, hash]);
return null;
};
```
The effect runs for the initial route and whenever the current route fields change. It does not run again for `reload()` or navigation to the same URL, and it does not observe the start of a navigation. Instant navigation commits the requested route before reconciliation, so a redirected instant navigation logs the optimistic route and then the reconciled one; keep the previous value if you only want landings.
Catch the promises returned by programmatic `push`, `replace`, and `reload` calls to handle fetch errors. There is no global failure callback for `` or browser back and forward; failed navigations surface through `ErrorBoundary` from `waku/router/client` (or your own boundary around the router). Do not treat promise resolution as a committed-route signal: rendering can still suspend, and a render-time redirect or 404 can start another navigation after the response is accepted. Observe the route fields above when you need the destination that was committed. To learn that a newer navigation cancelled yours while its response was pending, pass the experimental `unstable_onSuperseded` option to `push` or `replace`. For an instant navigation, its shell may already have painted. An accepted response is not cancelled merely because its components have not finished rendering.
## View Transitions
Waku commits a navigation inside a React transition, so animating one needs nothing from the router: wrap the part of the tree you want animated in React's `` and React drives the browser View Transitions API for you.
```tsx
'use client';
import { ViewTransition } from 'react';
import type { ReactNode } from 'react';
export const AnimatedPage = ({ children }: { children: ReactNode }) => (
{children}
);
```
Call `addTransitionType` alongside a `router.push()` to vary the animation per navigation. A browser without the View Transitions API just applies the update without animating it.
The prefetch and instant-navigation props in this guide are experimental and may change.