navigateToNotFound - greydragon888/real-router GitHub Wiki
-
What it does: Sets the router state to
UNKNOWN_ROUTEsynchronously, without going through the transition pipeline. The URL is preserved — the browser history entry is replaced, not pushed. -
When to use:
- When an API returns 404 for a resource that the URL points to (the URL is valid, but the resource doesn't exist)
- When application logic determines a resource is unavailable (permission denied, feature flag off, etc.)
- When you want to show a not-found state without changing the URL
This method is not for URL typos or unrecognized paths — those are handled automatically by start() and popstate events when allowNotFound: true. It's for programmatic 404: when your application code decides the resource doesn't exist.
router.navigateToNotFound(): State;
router.navigateToNotFound(path: string): State;| Parameter | Type | Required | Description |
|---|---|---|---|
path |
string |
No | The URL path to record in the not-found state. Defaults to the current state's path if not provided. |
-
Type:
State(synchronous — not a Promise) -
Successful result: Returns the new
Stateobject withname: UNKNOWN_ROUTE -
No rejection: This method never throws for navigation reasons. It only throws
ROUTER_NOT_STARTEDorROUTER_DISPOSED(see Possible Errors)
-
State change: Updates current router state to
UNKNOWN_ROUTEstate immediately -
Event triggers:
-
TRANSITION_SUCCESS— emitted directly (noTRANSITION_STARTprecedes it) - Plugins receive
onTransitionSuccess(toState, fromState, opts)whereopts.replace === true
-
-
Browser history: Always uses
replaceState— the not-found entry replaces the current history entry - Guards: None are executed — the transition pipeline is bypassed entirely
| Condition | Error Code | How to Handle |
|---|---|---|
| Router not started | ROUTER_NOT_STARTED |
Call only after router.start()
|
| Router disposed | ROUTER_DISPOSED |
Create a new router instance |
The most common use case. The URL is valid (route matched), but the resource behind it doesn't exist.
function ItemPage() {
const { route } = useRouteNode("items");
const { data, error } = useFetch(`/api/items/${route.params.id}`);
if (error?.status === 404) {
// URL stays at /items/10000 — user sees which address led to 404
// Synchronous — no Promise, returns State immediately
router.navigateToNotFound();
return null;
}
return <ItemDetails data={data} />;
}Why not just render a 404 component inline? Because navigateToNotFound():
- Sets router state to
UNKNOWN_ROUTE— every subscriber, plugin, anduseRouteNodereacts - Provides
transition.fromandtransition.segments— contextual error pages know where the user was - Keeps the URL — browser history reflects the actual failed path
- Replaces history entry — back button won't loop through 404 pages
The consumer of navigateToNotFound() output. Root component checks for UNKNOWN_ROUTE and renders the appropriate error page.
import { UNKNOWN_ROUTE } from "@real-router/core";
function App() {
const { route } = useRouteNode("");
if (route.name === UNKNOWN_ROUTE) {
return (
<NotFoundPage
context={route.transition?.from} // "items.view" — which route was active
path={route.path} // "/items/10000" — which URL failed
/>
);
}
return (
<Layout>
<Outlet />
</Layout>
);
}Server returns 403 but you want to show 404 — don't reveal that the resource exists.
function AdminPage() {
const { route } = useRouteNode("admin");
const { error } = useFetch(`/api/admin/${route.params.section}`);
if (error?.status === 403 || error?.status === 404) {
router.navigateToNotFound(); // Don't reveal admin routes exist
return null;
}
return <AdminPanel />;
}Feature flag or A/B test makes a route "not found" for some users.
function BetaFeaturePage() {
const { route } = useRouteNode("beta");
const { isEnabled } = useFeatureFlag("beta-feature");
if (!isEnabled) {
router.navigateToNotFound();
return null;
}
return <BetaFeature />;
}Show what went wrong, not just "page not found".
function NotFoundPage({ context, path }: { context?: string; path: string }) {
// context = route.transition.from — e.g. "items.view"
if (context?.startsWith("items")) {
return (
<p>
Item at {path} was not found.{" "}
<Link routeName="items">Browse all items</Link>
</p>
);
}
if (context?.startsWith("users")) {
return (
<p>
User not found. <Link routeName="users">View all users</Link>
</p>
);
}
return (
<p>
Page {path} does not exist. <Link routeName="home">Go home</Link>
</p>
);
}import { UNKNOWN_ROUTE } from "@real-router/core";
// Use in subscribe()
router.subscribe(({ route }) => {
if (route.name === UNKNOWN_ROUTE) {
analytics.track("404", { path: route.path, from: route.transition?.from });
}
});
// Use in guards — prevent navigating to a "hidden" route
lifecycle.addActivateGuard("secret", () => (toState) => {
if (!hasAccess()) {
// After guard blocks: navigate to not-found in catch handler
return false;
}
return true;
});| Method | When to use |
|---|---|
router.navigate() |
Navigate to a known route (async, goes through guards) |
router.navigateToDefault() |
Redirect to the default route (URL changes, async) |
router.start() |
Initial navigation — handles allowNotFound automatically |
router.canNavigateTo() |
Check if navigation is allowed without navigating |
-
navigateToNotFound()is synchronous — it returnsStatedirectly, not aPromise - It bypasses the transition pipeline entirely — no
TRANSITION_START, no guards, no FSM transition. OnlyTRANSITION_SUCCESSis emitted directly. This is intentional: when application logic determines a resource doesn't exist, deactivation guards (e.g., "unsaved changes?") are meaningless -
replace: trueis always used — 404 entries don't pollute browser history -
paramsis always{}— the path is instate.path, notstate.params.path - When navigating away from
UNKNOWN_ROUTEvianavigate(),replace: trueis automatically forced to prevent history accumulation
The resulting state has a special shape:
| Property | Value | Description |
|---|---|---|
name |
"@@router/UNKNOWN_ROUTE" |
Special constant, import as UNKNOWN_ROUTE
|
params |
{} |
Always empty |
path |
"/failed/url" |
The URL that led to 404 |
transition.from |
"previous.route" |
Previous route name (if any) |
transition.segments.deactivated |
["previous.route", "previous"] |
Segments that were left |
transition.segments.activated |
["@@router/UNKNOWN_ROUTE"] |
Always single-element |
| Aspect | navigateToNotFound() |
navigateToDefault() |
|---|---|---|
| URL | Preserved (user sees failed URL) | Changes to default route URL |
| Return |
State (sync) |
Promise<State> (async) |
| Guards | Skipped | Execute normally |
| Use case | "This resource doesn't exist" | "Go to starting page" |
| Transition metadata | Full (from, segments) |
Standard |
| History | Always replaces | Depends on options |