npm.io
0.10.1 • Published 1h ago

@real-router/sources

Licence
MIT
Version
0.10.1
Deps
2
Size
188 kB
Vulns
0
Weekly
585
Stars
6

@real-router/sources

npm npm downloads bundle size License: MIT

Framework-agnostic subscription layer for Real-Router. Reactive primitives compatible with useSyncExternalStore and vanilla JS.

Used internally by @real-router/react. Use this package directly when building integrations for other frameworks or vanilla JS applications.

Installation

npm install @real-router/sources

Peer dependency: @real-router/core

Quick Start

import { createRouter } from "@real-router/core";
import { createRouteSource } from "@real-router/sources";

const router = createRouter([
  { name: "home", path: "/" },
  { name: "users", path: "/users/:id" },
]);

await router.start("/");

const source = createRouteSource(router);
const unsubscribe = source.subscribe(() => {
  console.log("Route:", source.getSnapshot().route?.name);
});

Source Factories

Factory Returns Cache
createRouteSource(router) RouterSource<{ route, previousRoute }> not cached
createRouteNodeSource(router, node) RouterSource<{ route, previousRoute }> per-router + per-nodeName
createActiveRouteSource(router, name, params?, opts?) RouterSource<boolean> per-router + canonical-args
createTransitionSource(router) RouterSource<{ isTransitioning, isLeaveApproved, toRoute, fromRoute }> not cached (advanced)
getTransitionSource(router) same as above per-router — recommended for integrations
createErrorSource(router) RouterSource<{ error, toRoute, fromRoute, version }> not cached (advanced)
getErrorSource(router) same as above per-router — recommended for integrations
createDismissableError(router) RouterSource<{ error, toRoute, fromRoute, version, resetError }> per-router — dismissal-aware error source for RouterErrorBoundary-style UIs
createActiveNameSelector(router) ActiveNameSelector (selector API — subscribe(name, listener) / isActive(name) / destroy; not a RouterSource<T> — no getSnapshot()) per-router — O(1) active-name checker for Link fast-path

Plus utilities: DEFAULT_ACTIVE_OPTIONS, normalizeActiveOptions(opts?), canonicalJson(value), and the ActiveNameSelector type.

All factories return a RouterSource<T> except createActiveNameSelector, which returns an ActiveNameSelector (see Source Factories table above):

interface RouterSource<T> {
  subscribe(listener: () => void): () => void; // useSyncExternalStore-compatible
  getSnapshot(): T; // current value, synchronous
  destroy(): void; // no-op for cached wrappers; real teardown for create*
}

// createActiveNameSelector is the exception — its `subscribe` accepts a route-name
// argument and the active-state check lives on `isActive(name)` (no `getSnapshot()`).
interface ActiveNameSelector {
  subscribe(routeName: string, listener: () => void): () => void;
  isActive(routeName: string): boolean;
  destroy(): void;
}
Cached vs non-cached factories

Cached factories (createRouteNodeSource, createActiveRouteSource, getTransitionSource, getErrorSource) share a single source across all consumers of the same router. Multiple subscribe/unsubscribe pairs on the same instance share one router subscription. destroy() on the returned wrapper is a no-op — the underlying source lives as long as the router (the WeakMap entry releases on router GC).

Non-cached factories (createRouteSource, createTransitionSource, createErrorSource) return a fresh instance every call with real teardown on destroy() — use when you need an isolated source.

All route-state sources (createRouteSource, createRouteNodeSource, createActiveRouteSource) deduplicate via stabilizeState: same-path non-reload transitions preserve the snapshot reference and skip listener notifications. Reload navigations bypass dedup. See INVARIANTS.md #6.

Lazy vs Eager Subscription
  • createRouteSource, createRouteNodeSource, createActiveRouteSourcelazy: subscribe to the router on first listener, unsubscribe when all removed
  • createTransitionSource / getTransitionSourceeager: subscribes immediately (needs to track TRANSITION_START)
  • createErrorSource / getErrorSourceeager: subscribes immediately (needs to track TRANSITION_ERROR)
createActiveRouteSource Options
const source = createActiveRouteSource(router, "users", undefined, {
  strict: false, // default: false — match descendants too
  ignoreQueryParams: true, // default: true
  hash: undefined, // default: undefined — ignore URL fragment.
  //                 string → match iff state.context.url.hash equals it (#532).
});
Option Type Default Effect
strict boolean false When false, parent route is active when the current route is a descendant; when true, only an exact name match is active.
ignoreQueryParams boolean true Whether to drop query-string params before comparing.
hash string undefined When set, source is active iff route matches and state.context.url.hash equals this value. Requires a URL-publishing plugin (browser/navigation); under hash-plugin or memory-plugin (no context.url namespace), a non-undefined hash is always false.

Params are hashed with canonicalJson(), so {a: 1, b: 2} and {b: 2, a: 1} hit the same cache entry. BigInt/circular refs fall back to a fresh non-cached source with a working destroy() — call it to release the router subscription.

Usage Examples

With React (useSyncExternalStore)
import { useSyncExternalStore } from "react";
import { createRouteSource } from "@real-router/sources";

const source = createRouteSource(router);

function CurrentRoute() {
  const { route } = useSyncExternalStore(source.subscribe, source.getSnapshot);
  return <p>Current route: {route?.name}</p>;
}
With Vanilla JS
import { createRouteNodeSource } from "@real-router/sources";

// Only fires when navigating within the "users" subtree
const source = createRouteNodeSource(router, "users");

const unsubscribe = source.subscribe(() => {
  const { route } = source.getSnapshot();
  console.log("Users section:", route?.name);
});

unsubscribe(); // automatically unsubscribes from router
Transition Tracking
import { getTransitionSource } from "@real-router/sources";

// getTransitionSource — per-router cached. Safe to call destroy() multiple
// times; shared across all consumers in the same process.
const source = getTransitionSource(router);

source.subscribe(() => {
  const { isTransitioning, isLeaveApproved, toRoute, fromRoute } =
    source.getSnapshot();
  if (isTransitioning) {
    showSpinner();
  } else {
    hideSpinner();
  }
});
Error Tracking
import { getErrorSource } from "@real-router/sources";

const source = getErrorSource(router);

source.subscribe(() => {
  const { error, toRoute } = source.getSnapshot();
  if (error) {
    console.error(`Navigation to ${toRoute?.name} failed: ${error.code}`);
  }
});

Documentation

Package Description
@real-router/core Core router (required dependency)
@real-router/react React integration (uses sources internally)
@real-router/rx Observable API (state$, events$)

Contributing

See contributing guidelines for development setup and PR process.

License

MIT Oleg Ivanov

Keywords