navigateToNotFound - greydragon888/real-router GitHub Wiki

router.navigateToNotFound

1. Overview

  • What it does: Sets the router state to UNKNOWN_ROUTE synchronously, 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.

2. Signature

router.navigateToNotFound(): State;
router.navigateToNotFound(path: string): State;

3. Parameters

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.

4. Return Value

  • Type: State (synchronous — not a Promise)
  • Successful result: Returns the new State object with name: UNKNOWN_ROUTE
  • No rejection: This method never throws for navigation reasons. It only throws ROUTER_NOT_STARTED or ROUTER_DISPOSED (see Possible Errors)

5. Side Effects

  • State change: Updates current router state to UNKNOWN_ROUTE state immediately
  • Event triggers:
    • TRANSITION_SUCCESS — emitted directly (no TRANSITION_START precedes it)
    • Plugins receive onTransitionSuccess(toState, fromState, opts) where opts.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

6. Possible Errors

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

7. Usage Examples

Example 1: API returns 404

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, and useRouteNode reacts
  • Provides transition.from and transition.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

Example 2: Root-level 404 rendering

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>
  );
}

Example 3: Permission-based not-found (security through obscurity)

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 />;
}

Example 4: Conditional availability

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 />;
}

Example 5: Contextual error page using transition metadata

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>
  );
}

Example 6: UNKNOWN_ROUTE constant import

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;
});

8. Related Methods

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

9. Behavior

Key Points

  • navigateToNotFound() is synchronous — it returns State directly, not a Promise
  • It bypasses the transition pipeline entirely — no TRANSITION_START, no guards, no FSM transition. Only TRANSITION_SUCCESS is emitted directly. This is intentional: when application logic determines a resource doesn't exist, deactivation guards (e.g., "unsaved changes?") are meaningless
  • replace: true is always used — 404 entries don't pollute browser history
  • params is always {} — the path is in state.path, not state.params.path
  • When navigating away from UNKNOWN_ROUTE via navigate(), replace: true is automatically forced to prevent history accumulation

UNKNOWN_ROUTE State Shape

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

Difference from navigateToDefault()

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
⚠️ **GitHub.com Fallback** ⚠️