RouteView - greydragon888/real-router GitHub Wiki

RouteView

1. Overview

  • Name: RouteView
  • Purpose: Declarative route matching component with optional state preservation via keepAlive
  • When to use: For rendering content based on the active route segment without manual switch/case
  • Availability: All 6 framework adapters — @real-router/react (React 19.2+ for keepAlive via Activity), @real-router/preact (without keepAlive), @real-router/solid (without keepAlive), @real-router/vue (with keepAlive via native <KeepAlive>), @real-router/svelte (named snippets pattern instead of <Match>/<NotFound>), and @real-router/angular. Not available in @real-router/react/legacy.

2. Import and Basic Usage

import { RouteView } from "@real-router/react";
// or
import { RouteView } from "@real-router/preact";
// or
import { RouteView } from "@real-router/solid";
// or
import { RouteView } from "@real-router/vue";
// or
import { RouteView } from "@real-router/svelte";

// Angular
import { RouteView, RouteMatch, RouteNotFound } from "@real-router/angular";

function App() {
  return (
    <RouteView nodeName="">
      <RouteView.Match segment="users">
        <UsersPage />
      </RouteView.Match>
      <RouteView.Match segment="settings">
        <SettingsPage />
      </RouteView.Match>
      <RouteView.NotFound>
        <NotFoundPage />
      </RouteView.NotFound>
    </RouteView>
  );
}

3. Props

<RouteView>

Prop Type Required Default Description
nodeName string Yes Route tree node to subscribe to. "" for root.
children ReactNode Yes <RouteView.Match> and <RouteView.NotFound> elements

<RouteView.Match>

Prop Type Required Default Description
segment string Yes Route segment to match against
exact boolean No false Exact match only (no descendants)
keepAlive boolean No false Preserve component state when deactivated (React Activity)
fallback ReactNode No Fallback UI while content is loading (wrapped in Suspense)
children ReactNode Yes Content to render when matched

<RouteView.NotFound>

Prop Type Required Default Description
children ReactNode Yes Content to render on UNKNOWN_ROUTE

4. Children

  • Type: ReactNode
  • Description: Only <RouteView.Match> and <RouteView.NotFound> elements are recognized. Other elements are traversed for nested Match/NotFound (e.g., inside <Fragment>). Non-element children are ignored.
<RouteView nodeName="">
  <RouteView.Match segment="home">
    <HomePage />
  </RouteView.Match>
  {/* Fragment wrappers are transparent — Match inside is found */}
  <>
    <RouteView.Match segment="about">
      <AboutPage />
    </RouteView.Match>
  </>
</RouteView>

4.1. Fallback Prop (Suspense Integration)

The fallback prop wraps matched content in a <Suspense> boundary, enabling async component loading:

React/Preact:

<RouteView.Match segment="users" fallback={<Spinner />}>
  <UsersPage /> {/* Wrapped in <Suspense fallback={<Spinner />}> */}
</RouteView.Match>

Solid:

<Match segment="users" fallback={<Spinner />}>
  <UsersPage /> {/* Wrapped in <Suspense fallback={<Spinner />}> */}
</Match>

Vue:

<RouteView.Match segment="users" :fallback="Spinner">
  <UsersPage /> <!-- Wrapped in <Suspense> -->
</RouteView.Match>

Svelte:

<Lazy loader={async () => (await import('./UsersPage')).default} fallback={Spinner} />

Svelte uses a different pattern -- <Lazy> component inside snippets instead of a fallback prop.

Angular:

<!-- Angular: uses ng-template with directives instead of sub-components -->
<route-view [routeNode]="''">
  <ng-template routeMatch="home"><app-home /></ng-template>
  <ng-template routeMatch="users"><app-users /></ng-template>
  <ng-template routeNotFound><app-not-found /></ng-template>
</route-view>

Angular note: The input is routeNode (aliased from nodeName) because nodeName is a read-only property on HTMLElement. Route matching uses ng-template with routeMatch and routeNotFound structural directives collected via contentChildren.

5. Rendering

What It Renders

  • HTML element: Does not render its own DOM element
  • Output: A fragment containing matched children

Matching Logic

RouteView subscribes to a route node via useRouteNode(nodeName) and matches children against the active route:

  1. Collects all <RouteView.Match> and <RouteView.NotFound> from children (recursively unwraps fragments)
  2. Iterates Match elements — first match wins (subsequent matches are skipped unless keepAlive)
  3. Segment matching: constructs fullSegmentName as nodeName.segment (or just segment if nodeName is "")
  4. Default matching uses startsWithSegment (prefix match). With exact, uses strict equality.

Without keepAlive

Only the first matched segment is rendered. All others return null.

// Route: "users.profile"
<RouteView nodeName="">
  <RouteView.Match segment="users">
    {" "}
    {/* Rendered (prefix match) */}
    <UsersPage />
  </RouteView.Match>
  <RouteView.Match segment="settings">
    {" "}
    {/* null */}
    <SettingsPage />
  </RouteView.Match>
</RouteView>

With keepAlive

Active match is rendered as <Activity mode="visible">. Previously active keepAlive matches are rendered as <Activity mode="hidden"> — hidden but not unmounted.

// Route: "settings", previously visited "users"
<RouteView nodeName="">
  <RouteView.Match segment="users" keepAlive>
    {" "}
    {/* <Activity mode="hidden"> */}
    <UsersPage />
  </RouteView.Match>
  <RouteView.Match segment="settings" keepAlive>
    {" "}
    {/* <Activity mode="visible"> */}
    <SettingsPage />
  </RouteView.Match>
</RouteView>

keepAlive Behavior Table

React (Activity):

keepAlive Route matches Previously activated Result
true Yes <Activity mode="visible">
true No Yes <Activity mode="hidden">
true No No null (lazy mount)
false Yes <Fragment>{children}</Fragment>
false No null

Vue (native KeepAlive):

keepAlive Route matches Result
root true Yes <KeepAlive><Wrapper>...</Wrapper></KeepAlive>
root true No Vue <KeepAlive> caches the inactive instance
per-match Yes <KeepAlive><Wrapper>...</Wrapper></KeepAlive>
per-match No Empty <KeepAlive> placeholder preserves cache
false Yes <Fragment>{children}</Fragment>
false No null

NotFound

<RouteView.NotFound> renders only when:

  • No Match matched the active route AND
  • The route name is UNKNOWN_ROUTE

If multiple <RouteView.NotFound> are present, the last one is used.

6. Styling

Component does not render DOM elements and does not support styling directly. Style the children instead.

7. Accessibility

Component does not directly affect accessibility — it's a structural routing component. Ensure child components maintain proper focus management and ARIA attributes.

8. Dependencies and Context

Required Providers

Provider Required Description
RouterProvider Yes For accessing router via useRouteNode

Used Hooks

  • useRouteNode(nodeName) — subscribes to the specified route node, re-renders only when that node's active/inactive state changes
  • useRef — tracks which keepAlive segments have been activated (lazy mount)

React APIs

  • Activity (React 19.2+) — wraps keepAlive matches for state preservation
  • Children.toArray, isValidElement — child element collection
  • Fragment — key-able wrapper for non-keepAlive matches

9. Events

Component does not generate its own events. Route changes are handled by the router and propagated via useRouteNode.

10. Behavior

Main Scenarios

  • Renders the first matched <RouteView.Match> based on the active route
  • Returns null if useRouteNode returns no route (node is inactive)
  • Supports keepAlive for preserving component state across navigations
  • Renders <RouteView.NotFound> for unknown routes
  • Supports nesting — RouteView inside RouteView for hierarchical route trees

Segment Matching

// Route: "users.profile"
// nodeName: ""

<RouteView.Match segment="users">          {/* Match: "users" is prefix of "users.profile" */}
<RouteView.Match segment="users" exact>    {/* No match: "users" !== "users.profile" */}
<RouteView.Match segment="settings">       {/* No match: "settings" is not prefix */}

Segment matching is dot-boundary safe: segment="users" does not match route "users2.list".

Lazy Mount (keepAlive)

A keepAlive Match is not rendered until its route has been activated at least once. This prevents pre-rendering all keepAlive segments on first load.

Edge Cases

  • Empty RouteView (no children) returns null
  • Non-Match/non-NotFound children are ignored (but traversed for nested Match/NotFound)
  • When route is undefined (node inactive), returns null — all keepAlive hidden elements are also unmounted
  • Multiple <RouteView.NotFound> — last one wins

Guarantees

  • First match wins — only one Match is visible at a time
  • keepAlive state survives navigation within the same RouteView
  • Dot-boundary safe segment matching via startsWithSegment from @real-router/route-utils

11. Related Components

Component/Hook Relationship
useRouteNode Used internally for route node subscription
RouterProvider Required provider for router context
Link Navigation component — triggers route changes
useRoute Alternative: full route state subscription

12. Usage Examples

Basic Example

import { RouteView } from "@real-router/react";

function App() {
  return (
    <RouteView nodeName="">
      <RouteView.Match segment="home">
        <HomePage />
      </RouteView.Match>
      <RouteView.Match segment="users">
        <UsersPage />
      </RouteView.Match>
      <RouteView.NotFound>
        <NotFoundPage />
      </RouteView.NotFound>
    </RouteView>
  );
}

keepAlive — Preserve State

React (Activity):

function App() {
  return (
    <RouteView nodeName="">
      <RouteView.Match segment="users" keepAlive>
        <UsersPage /> {/* State preserved when navigating away */}
      </RouteView.Match>
      <RouteView.Match segment="settings">
        <SettingsPage /> {/* Unmounts normally */}
      </RouteView.Match>
    </RouteView>
  );
}

Vue (native KeepAlive):

Root-level keepAlive — all matches are kept alive:

<template>
  <RouteView nodeName="" keepAlive>
    <RouteView.Match segment="users">
      <UsersPage />
      <!-- State preserved when navigating away -->
    </RouteView.Match>
    <RouteView.Match segment="settings">
      <SettingsPage />
      <!-- Also preserved -->
    </RouteView.Match>
  </RouteView>
</template>

Per-match keepAlive — only specific matches are kept alive:

<template>
  <RouteView nodeName="">
    <RouteView.Match segment="users" keepAlive>
      <UsersPage />
      <!-- State preserved when navigating away -->
    </RouteView.Match>
    <RouteView.Match segment="settings">
      <SettingsPage />
      <!-- Unmounts normally -->
    </RouteView.Match>
  </RouteView>
</template>

When navigating users -> settings -> users:

  1. <UsersPage /> mounts (visible)
  2. Navigate to settings: <UsersPage /> becomes hidden (not unmounted), <SettingsPage /> mounts
  3. Navigate back to users: <UsersPage /> becomes visible instantly — scroll position, form state, etc. preserved

Nested RouteView

function App() {
  return (
    <RouteView nodeName="">
      <RouteView.Match segment="users" keepAlive>
        <UsersLayout>
          <RouteView nodeName="users">
            <RouteView.Match segment="list" keepAlive>
              <UsersList />
            </RouteView.Match>
            <RouteView.Match segment="profile">
              <UserProfile />
            </RouteView.Match>
          </RouteView>
        </UsersLayout>
      </RouteView.Match>
    </RouteView>
  );
}

keepAlive works independently at each nesting level.

Exact Matching

<RouteView nodeName="users">
  {/* Only matches "users.list" exactly, not "users.list.details" */}
  <RouteView.Match segment="list" exact>
    <UsersList />
  </RouteView.Match>
  {/* Matches "users.profile" and "users.profile.edit" */}
  <RouteView.Match segment="profile">
    <UserProfile />
  </RouteView.Match>
</RouteView>

Multiple keepAlive

<RouteView nodeName="">
  <RouteView.Match segment="users" keepAlive>
    <UsersPage />
  </RouteView.Match>
  <RouteView.Match segment="settings" keepAlive>
    <SettingsPage />
  </RouteView.Match>
  <RouteView.Match segment="about">
    <AboutPage />
  </RouteView.Match>
</RouteView>

Both users and settings preserve state. about unmounts when deactivated.

Accessing Route Data Inside Children

<RouteView.Match segment="profile" keepAlive>
  <UserProfile />
</RouteView.Match>;

function UserProfile() {
  const { route } = useRouteNode("users.profile");
  // route is defined when visible, undefined when hidden
  // Component is NOT unmounted when hidden — state preserved
  return <div>User: {route?.params.id}</div>;
}

Reacting to Hide/Show (keepAlive)

React:

function UsersList() {
  useEffect(() => {
    // Called when becoming visible
    refreshData();

    return () => {
      // Called when hidden AND when unmounted
    };
  }, []);

  return <List />;
}

React Activity guarantees: visible -> hidden triggers effect cleanup, hidden -> visible re-runs effect setup.

Vue:

<script setup>
import { onActivated, onDeactivated } from "vue";

onActivated(() => {
  // Called when becoming visible (KeepAlive reactivation)
  refreshData();
});

onDeactivated(() => {
  // Called when hidden (KeepAlive deactivation)
});
</script>

<template>
  <List />
</template>

Vue <KeepAlive> provides onActivated / onDeactivated lifecycle hooks for reacting to visibility changes.

Anti-patterns

// Don't use RouteView outside RouterProvider
function Bad() {
  return (
    <RouteView nodeName="">
      <RouteView.Match segment="home">
        <Home />
      </RouteView.Match>
    </RouteView>
  ); // Error: useRouteNode requires RouterProvider
}

// Wrap in RouterProvider
function Good() {
  return (
    <RouterProvider router={router}>
      <RouteView nodeName="">
        <RouteView.Match segment="home">
          <Home />
        </RouteView.Match>
      </RouteView>
    </RouterProvider>
  );
}
// Don't use render props — not supported
function Bad() {
  return (
    <RouteView.Match segment="users">
      {(route) => <UsersPage route={route} />}
    </RouteView.Match>
  );
}

// Use hooks inside child components
function Good() {
  return (
    <RouteView.Match segment="users">
      <UsersPage />
    </RouteView.Match>
  );
}

function UsersPage() {
  const { route } = useRouteNode("users");
  return <div>{route?.name}</div>;
}
// Don't import from legacy entry — RouteView is not available
import { RouteView } from "@real-router/react/legacy"; // Error!

// Use main entry point (React 19.2+)
import { RouteView } from "@real-router/react";
⚠️ **GitHub.com Fallback** ⚠️