JSPM

  • Created
  • Published
  • Downloads 2114
  • Score
    100M100P100Q112036F
  • License MIT

Persist query parameters across route transitions

Package Exports

  • @real-router/persistent-params-plugin

Readme

@real-router/persistent-params-plugin

npm npm downloads bundle size License: MIT

Automatically persist query parameters across all navigation transitions in Real-Router.

// Without plugin:
router.navigate("products", { lang: "en", theme: "dark" });
router.navigate("cart");
// URL: /cart  — lang and theme are lost

// With plugin:
router.usePlugin(persistentParamsPluginFactory(["lang", "theme"]));
router.navigate("products", { lang: "en", theme: "dark" });
router.navigate("cart");
// URL: /cart?lang=en&theme=dark  — automatically preserved

Installation

npm install @real-router/persistent-params-plugin

Peer dependency: @real-router/core

Quick Start

import { createRouter } from "@real-router/core";
import { persistentParamsPluginFactory } from "@real-router/persistent-params-plugin";

const router = createRouter(routes);

// Array — values set on first navigation
router.usePlugin(persistentParamsPluginFactory(["lang", "theme"]));

// Object — with default values
router.usePlugin(persistentParamsPluginFactory({ lang: "en", theme: "light" }));

Configuration

Config Type Description Example
string[] Parameter names, initial values undefined ["lang", "theme"]
Record<string, primitive> Parameter names with defaults { lang: "en" }

Allowed value types: string, number, boolean, undefined (to remove a param).

Behavior

// Persist — saved on first navigation
router.navigate("page1", { lang: "en" });     // saved: lang=en

// Carry — auto-injected into subsequent navigations
router.navigate("page2");                      // URL: /page2?lang=en

// Update — explicit values override saved ones
router.navigate("page3", { lang: "fr" });      // URL: /page3?lang=fr, saved: lang=fr

// Remove — pass undefined to stop persisting
router.navigate("page4", { lang: undefined }); // lang removed permanently

Note: Removal is permanent for the plugin lifetime — but only once the removal navigation actually commits. Once undefined is passed and the navigation succeeds, the param is no longer tracked, even if passed again later. If that navigation is rejected by a guard or superseded by a concurrent navigate, the param stays persisted (the removal rolls back).

Use Cases

Multilingual App

router.usePlugin(persistentParamsPluginFactory({ lang: "en" }));

router.navigate("settings", { lang: "fr" });
router.navigate("products");   // ?lang=fr
router.navigate("cart");        // ?lang=fr

UTM Tracking

router.usePlugin(
  persistentParamsPluginFactory(["utm_source", "utm_medium", "utm_campaign"]),
);

// User arrives: /?utm_source=google&utm_medium=cpc
router.navigate("products");    // UTM params preserved
router.navigate("checkout");    // UTM params preserved

Cleanup

const unsubscribe = router.usePlugin(persistentParamsPluginFactory(["mode"]));

// Later — restore original router behavior
unsubscribe();

State Context: state.context.persistentParams

The plugin publishes a snapshot of the current persistent params to state.context.persistentParams after each successful transition. This lets components distinguish persistent params from route-specific params.

import { createRouter } from "@real-router/core";
import { persistentParamsPluginFactory } from "@real-router/persistent-params-plugin";

const router = createRouter(routes);
router.usePlugin(persistentParamsPluginFactory({ lang: "en", theme: "light" }));

router.subscribe(({ route, previousRoute }) => {
  const { params, context } = route;

  // params contains BOTH route-specific and persistent params merged together
  console.log(params);              // { id: "42", lang: "en", theme: "light" }

  // context.persistentParams contains ONLY the persistent params
  console.log(context.persistentParams); // { lang: "en", theme: "light" }
});

Timing: Written in onTransitionSuccess (before subscriber callbacks fire). Always reflects the latest committed values.

Type: Importing @real-router/persistent-params-plugin augments StateContext with persistentParams?: Params, providing full type safety.

Composition with @real-router/search-schema-plugin

This plugin injects persistent params via a forwardState interceptor; search-schema-plugin validates params via its own forwardState interceptor. Core composes interceptors LIFO (last-registered = outermost), so registration order decides whether persistent params are validated:

// RECOMMENDED — register persistent-params FIRST, search-schema SECOND:
router.usePlugin(persistentParamsPluginFactory({ page: 1 }));
router.usePlugin(searchSchemaPlugin());
// search-schema is outermost → validates the injected persistent params (invalid ones stripped)

// ALTERNATIVE — persistent-params outermost → persistent params bypass the schema:
router.usePlugin(searchSchemaPlugin());
router.usePlugin(persistentParamsPluginFactory({ page: 1 }));

Register this plugin before search-schema-plugin to have persistent params validated (the safer default); after it only when they must deliberately skip validation.

Caveat: the recommended order validates state.params, not state.path. This plugin also registers a buildPath interceptor, which search-schema-plugin does not wrap — so an invalid persisted value is stripped from state.params but still reaches state.path (persistent, reload-stable). Give persisted keys a defaultParams on schema'd routes to close it (core's merge overrides the injected value). Also: the alternative-order leak only affects keys without a route default — stored params fill under incoming ones, so a key with a default is supplied by core and never leaks. (#1231)

Documentation

Full documentation: Wiki — persistent-params-plugin

Package Description
@real-router/core Core router (required peer dependency)
@real-router/browser-plugin Browser History API integration

Contributing

See contributing guidelines for development setup and PR process.

License

MIT © Oleg Ivanov