useRouteNode - greydragon888/real-router GitHub Wiki

useRouteNode

1. Overview

  • Name: useRouteNode
  • Purpose: Subscribe to changes of a specific route node with optimization — component re-renders only when the specified node is affected
  • When to use: When a component needs to react only to changes in a specific part of the route hierarchy, avoiding unnecessary re-renders

2. Signature

function useRouteNode(nodeName: string): RouteContext;
import { useRouteNode } from "@real-router/react";
// or
import { useRouteNode } from "@real-router/preact";
// or
import { useRouteNode } from "@real-router/solid";
// or
import { useRouteNode } from "@real-router/vue";
// or
import { useRouteNode } from "@real-router/svelte";

// Angular
import { injectRouteNode } from "@real-router/angular";

Solid.js note: In Solid, useRouteNode() returns reactive state as Accessor<RouteContext>. Call the accessor to read: const routeState = useRouteNode("users"); routeState().route?.name

Vue note: In Vue, useRouteNode() returns { navigator, route: ShallowRef, previousRoute: ShallowRef }. Access refs via .value: const { route } = useRouteNode("users"); route.value?.name

Angular note: injectRouteNode(name) returns the same RouteSignals shape as injectRoute(). Must be called in injection context. Creates a per-component subscription -- destroyed automatically via DestroyRef.

Basic Example

function UsersLayout() {
  const { route, previousRoute, navigator } = useRouteNode("users");

  // Component updates only on navigation within users.*
  return (
    <div>
      <h1>Users Section</h1>
      {route ? <Outlet /> : <p>Select a user</p>}
    </div>
  );
}

3. Parameters

Parameter Type Required Description
nodeName string Yes Route node name to subscribe to

Parameter Details

nodeName

  • Empty string "": Root node — reacts to all route changes
  • Node name (e.g., "users"): Reacts only when route starts with this node
  • Nested node (e.g., "admin.settings"): Reacts to admin.settings and its descendants

Value examples:

nodeName Reacts to Doesn't react to
"" All routes
"users" users, users.list, users.view home, items
"admin.settings" admin.settings, admin.settings.security admin, admin.dashboard

4. Return Value

  • Type: RouteContext
  • Description: Object with route state and navigator instance

Return Value Structure

Field Type Description
navigator Navigator Navigator instance for navigation and state access
route State | undefined Current route if node is active; undefined if node is inactive
previousRoute State | undefined Previous route

route Specifics

Unlike useRoute, the route field will be undefined when the specified node is inactive:

// At route "home"
const { route } = useRouteNode("users");
// route === undefined (node "users" is inactive)

// At route "users.list"
const { route } = useRouteNode("users");
// route === { name: "users.list", ... } (node is active)

5. Dependencies and Context

Required Providers

Provider Required Description
RouterProvider Yes Must wrap the component using the hook

Used Hooks

  • useRouter() — getting router instance
  • useSyncExternalStore — optimized subscription to router state
  • useMemo — memoization for reference stability

6. Re-render Behavior

  • Reference stability: Returned RouteContext object is stable between renders if state hasn't changed
  • Update triggers: Component re-renders only when:
    • Navigation affects the specified node
    • Route parameters change within the node
    • Node becomes active/inactive
    • reload: true option is used
  • Memoization: Hook automatically memoizes result
  • Shared subscription: Internally backed by createRouteNodeSource(router, nodeName) from @real-router/sources, which is cached per (router, nodeName). N components calling useRouteNode("users") against the same router share one router subscription — not N.

When Re-render Does NOT Happen

// With useRouteNode("users"):
// - Navigation home → items — NO re-render
// - Navigation items → items.detail — NO re-render

When Re-render DOES Happen

// With useRouteNode("users"):
// - Navigation home → users.list — re-render (node became active)
// - Navigation users.list → users.view — re-render (change within node)
// - Navigation users.view → home — re-render (node became inactive)

7. Possible Errors

Condition Error How to Avoid
Hook called outside RouterProvider "useRouter must be used within a RouterProvider" Wrap component in RouterProvider

8. Behavior

Main Scenarios

  • Root node ("") receives all route updates
  • Specific node receives updates only for its branch
  • route becomes undefined when node is inactive
  • previousRoute preserves last route before leaving node

Edge Cases

  • Router not started: route and previousRoute will be undefined
  • Dynamic nodeName change: Hook correctly re-subscribes
  • Multiple hooks for same node: All receive same values
  • Deeply nested nodes: Correctly track activity
  • reload: true: Triggers update even on same route

Guarantees

  • Reference stability on re-render without state changes
  • Correct previousRoute tracking through navigation chain
  • Independent operation of parallel nodes

9. Related Hooks

Hook When to Use Instead
useRoute When you need subscription to all route changes
useRouter When you only need router instance without subscription

10. Usage Examples

Basic Example — Section Layout

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

function UsersLayout() {
  const { route, navigator } = useRouteNode("users");

  if (!route) {
    // Node is inactive — can show placeholder or redirect
    return null;
  }

  return (
    <div className="users-layout">
      <nav>
        <button onClick={() => navigator.navigate("users.list")}>List</button>
        <button onClick={() => navigator.navigate("users.create")}>
          Create
        </button>
      </nav>
      <main>{/* Content based on route.name */}</main>
    </div>
  );
}

Re-render Optimization in Deep Hierarchy

// Each Layout subscribes only to its node
function AdminLayout() {
  const { route } = useRouteNode("admin");
  // Re-renders only on changes in admin.*
  return <div>{/* ... */}</div>;
}

function SettingsLayout() {
  const { route } = useRouteNode("admin.settings");
  // Re-renders only on changes in admin.settings.*
  return <div>{/* ... */}</div>;
}

function SecuritySettings() {
  const { route } = useRouteNode("admin.settings.security");
  // Re-renders only on changes in admin.settings.security.*
  return <div>{/* ... */}</div>;
}

Tracking Section Entry/Exit

function UsersSection() {
  const { route, previousRoute } = useRouteNode("users");

  useEffect(() => {
    if (route && !previousRoute?.name?.startsWith("users")) {
      console.log("Entered users section");
      analytics.track("users_section_entered");
    }
  }, [route, previousRoute]);

  useEffect(() => {
    if (!route && previousRoute?.name?.startsWith("users")) {
      console.log("Left users section");
    }
  }, [route, previousRoute]);

  return route ? <UsersContent /> : null;
}

Dynamic Node Switching

function DynamicNodeSubscription({ section }: { section: string }) {
  const { route } = useRouteNode(section);

  // Hook automatically re-subscribes when section changes
  return (
    <div>
      <p>Watching: {section}</p>
      <p>Active route: {route?.name ?? "none"}</p>
    </div>
  );
}

Anti-patterns

// Don't use useRouteNode for all routes
function Bad() {
  const { route } = useRouteNode(""); // Equivalent to useRoute
  return <div>{route?.name}</div>;
}

// Use useRoute for global subscription
function Good() {
  const { route } = useRoute();
  return <div>{route?.name}</div>;
}
// Don't check activity manually
function Bad() {
  const { route } = useRoute();
  const isUsersActive = route?.name?.startsWith("users");
  // Component re-renders on ANY navigation
}

// Use useRouteNode for optimization
function Good() {
  const { route } = useRouteNode("users");
  // route will be undefined if node is inactive
  // Component does NOT re-render on navigation outside users
}
// Don't create redundant subscriptions
function Bad() {
  const { route: route1 } = useRouteNode("users");
  const { route: route2 } = useRouteNode("users"); // Duplication!
}

// Use one hook and pass data via props/context
function Good() {
  const { route } = useRouteNode("users");
  return <ChildComponent route={route} />;
}

11. Migration from router5

Comparison of @real-router/react/useRouteNode with react-router5/useRouteNode from router5.

Version Comparison

router5 (react-router5) Real Router (@real-router/react)
Export export default function useRouteNode() export function useRouteNode()
Signature useRouteNode(nodeName: string): RouteContext useRouteNode(nodeName: string): RouteContext
route type State (always defined) State | undefined
previousRoute type State | null State | undefined
Subscription useState + useEffect useSyncExternalStore
Return field router { router: Router, ... } Replaced by navigator: Navigator

1. Breaking Changes

Severity What Changed Was Now Impact
CRITICAL Export type export default export function (named) All imports will break
HIGH Context validation Returns undefined router if no provider Throws Error Code without provider will crash
HIGH route behavior on inactive node Always State (last route) undefined when node is inactive Activity check logic will change
MEDIUM previousRoute type State | null State | undefined Need to replace null checks
LOW External dependency router5-transition-path Built-in router.shouldUpdateNode() No impact on user
HIGH Return field router router: Router in return value navigator: Navigator Update destructuring and method calls

Parameter Changes

Parameter nodeName unchanged.

Return Value Changes

Field Change Migration
route On inactive node returns undefined instead of last route Use route directly to check node activity
previousRoute Type State | nullState | undefined Replace !== null with truthiness check
router Field removed — replaced by navigator: Navigator Use navigator instead; it provides navigate, getState, subscribe

Examples

// Code that will break (import)
import useRouteNode from "react-router5";

// Code after migration
import { useRouteNode } from "@real-router/react";
// Code that will break (activity check)
const { route } = useRouteNode("users");
const isActive = route.name.startsWith("users"); // route was always defined

// Code after migration
const { route } = useRouteNode("users");
const isActive = !!route; // route === undefined when node is inactive
// or
if (route) {
  // node is active
}
// Code that will break (previousRoute)
if (previousRoute !== null) {
  /* ... */
}

// Code after migration
if (previousRoute) {
  /* ... */
}

2. Implementation Changes

Subscription Mechanism

  • Was: useState + useEffect with manual subscription via router.subscribe()
  • Now: useSyncExternalStore — modern React API for external stores

Advantages of New Approach

  • Concurrent Mode compatibility: useSyncExternalStore works correctly with Suspense and concurrent rendering
  • No tearing: Guarantees UI consistency even with fast updates
  • Fewer bugs: Removed potential race conditions from manual subscription

New Behavior on Inactive Node

  • Was: route contained last route regardless of node activity
  • Now: route becomes undefined when node is inactive

This change simplifies component logic:

// New approach — simpler
if (route) {
  // node is active, show content
}

Optimizations

  • Selector caching: shouldUpdate function cached per router and node name
  • Stable references: Result memoized via useMemo
  • Fail-fast validation: Clear error when provider is missing

3. Dependency Changes

Dependency Was Now
Router package router5 @real-router/core
React bindings react-router5 @real-router/react
External utility router5-transition-path.shouldUpdateNode Built-in shouldUpdateNode() from router

4. Migration Guide

Checklist

  • Replace import useRouteNode from 'react-router5' with import { useRouteNode } from '@real-router/react'
  • Change activity check: use if (route) instead of checking route.name
  • Replace !== null checks with truthiness check for previousRoute
  • Ensure component is wrapped in RouterProvider
  • Remove extra activity checks — route === undefined means inactive node

Step-by-Step Migration

  1. Update imports: Replace default import with named import
  2. Simplify activity checks: Instead of route.name.startsWith(node) use !!route
  3. Update null checks: Replace with undefined or truthiness checks
  4. Check providers: Ensure RouterProvider exists
  5. Test edge cases: Check behavior on node entry/exit

5. Summary

Category Status
Breaking Changes CRITICAL (export, route behavior)
New parameters None
Removed parameters None
Behavior changes route=undefined on inactive node
Optimizations useSyncExternalStore, caching, memoization

Maximum severity: CRITICAL — import changes and activity check logic changes required

⚠️ **GitHub.com Fallback** ⚠️