matchPath - greydragon888/real-router GitHub Wiki

getPluginApi().matchPath

1. Overview

  • What it does: Matches a URL path against the route tree and returns a State object
  • When to use:
    • Popstate event with no history.state — plugin needs to determine the route from the URL
    • Deep link handling — resolve a URL to a route state
    • SSR initial state — match the incoming request URL to a route
  • Access: getPluginApi(router).matchPath(path) — this is a plugin API method, not a router method

2. Signature

import { getPluginApi } from "@real-router/core/api";

const pluginApi = getPluginApi(router);

pluginApi.matchPath<P extends Params = Params>(
  path: string
): State<P> | undefined

Parameters

Parameter Type Required Description
path string Yes URL path to match (e.g., /users/123?tab=profile)

Return Value

  • Match found: Frozen State object with route information
  • No match: undefined
// Returned State structure
{
  name: "users.profile",        // Route name
  params: { id: "123" },        // Decoded parameters
  path: "/users/123",           // URL path (possibly rewritten)
}
// Segment parameter metadata is stored internally (WeakMap), not on the State object

3. Possible Errors

Condition Error
path not a string TypeError: [real-router] matchPath: path must be a string, got {type}
Decoder throws Error propagated (not suppressed)

Validation requires @real-router/validation-plugin. Without the plugin, no argument validation is performed.

matchPath does not check disposed state — it works even after router.dispose().

4. Related Methods

Method Description
buildState Validate route and get segment metadata
forwardState Resolve forwarding and merge params
buildPath Reverse operation — route name to URL
buildNavigationState Build full State without navigation

5. Behavior

Route Decoders

If the matched route has decodeParams, it is applied to the matched parameters:

import { getPluginApi, getRoutesApi } from "@real-router/core/api";

const api = getPluginApi(router);
const routesApi = getRoutesApi(router);

routesApi.add({
  name: "user",
  path: "/user/:id",
  decodeParams: (p) => ({ id: Number.parseInt(p.id, 10) }),
});

const state = api.matchPath("/user/123");
// state.params.id === 123 (number, not string)

URL Decoding

Parameters are automatically URL-decoded:

const api = getPluginApi(router);

api.matchPath("/user/%E6%97%A5%E6%9C%AC%E8%AA%9E");
// state.params.name === "日本語"

Forwarding

If the matched route has forwardTo, the returned state reflects the target route. Forwarding is resolved through the forwardState function on the router internals, so plugins that override forwardState (e.g., persistent-params-plugin) can intercept during path matching.

import { getPluginApi, getRoutesApi } from "@real-router/core/api";

const api = getPluginApi(router);
const routesApi = getRoutesApi(router);

routesApi.update("old-users", { forwardTo: "users" });
const state = api.matchPath("/old-users/123");
// state.name === "users" (forwarded)

Path Normalization

When rewritePathOnMatch: true (default), the returned state.path is rebuilt via buildPath, applying forwardTo aliases, encoders, defaultParams, and trailing-slash normalization:

const api = getPluginApi(router);

const state = api.matchPath("/user/123/");
// With trailingSlash: "never"    → state.path === "/user/123"
// With trailingSlash: "always"   → state.path === "/user/123/"
// With trailingSlash: "preserve" (default) → state.path === "/user/123/" — the source path's trailing-slash wins

The "preserve" case is honoured even though the matcher builds the canonical path without a trailing slash: the source path's trailing-slash choice is re-attached afterwards. See RouterOptions — trailingSlash.

Router Options That Affect Matching

Option Effect
caseSensitive Whether /Users matches route /users
trailingSlash How trailing slashes are handled
queryParamsMode "default" accepts any params, "strict" only declared
rewritePathOnMatch Whether state.path is rebuilt from the matched route

6. Migration

The matchPath method moved from the router instance to the plugin API:

// Before
const state = router.matchPath(path);

// After
import { getPluginApi } from "@real-router/core/api";

const api = getPluginApi(router);
const state = api.matchPath(path);

Plugin Example

import { getPluginApi } from "@real-router/core/api";

import type { PluginFactory } from "@real-router/core";

const myBrowserPlugin: PluginFactory = (router) => {
  const api = getPluginApi(router);

  async function onExternalUrlChange(url: string) {
    const path = extractPath(url);
    const state = api.matchPath(path);

    if (!state) {
      router.navigateToDefault({ replace: true });
      return;
    }

    try {
      await api.navigateToState(state, router.getState(), { replace: true });
    } catch {
      // Handle navigation error
    }
  }

  return {
    onStart() {
      window.addEventListener("customnavigation", onExternalUrlChange);
    },
    teardown() {
      window.removeEventListener("customnavigation", onExternalUrlChange);
    },
  };
};