Changelog
[2.7.4] 2026-05-26 Fixed (5) · Added (16)
Fixed
RouterWrapperevent-subscription leak (BIG): the previous implementation calledrouterEvents.onRendering(...)inside the render body via an IIFE, which created a new event subscription on every React render and only cleaned up the most recent one — leaking subscriptions under React 18 concurrent rendering andStrictMode’s double-render pass. Rebuilt the component onuseSyncExternalStorewith a single module-level store.subscribeopens at most onerouter.renderingsubscription (refcounted by listener count) and cleans up when the last subscriber detaches;getSnapshotreturns the current{ Layout, content, isLoading }triple. All original side effects (route key generation,router.activeRoutemutation,"rendered"event firing onsetTimeout(0), content caching, middleware execution) are preserved on the store’s mutation path. (src/components/RouterWrapper.tsx)- Matcher pattern-cache invalidation: the module-level
cache: any = {}insrc/matcher.tswas never cleared whenrouter.setMatcher(...)replaced the matcher, so swapping matchers would silently keep using regexps compiled by the previous matcher. The cache is now scoped per matcher reference via aWeakMap<UrlMatcher, ...>, so a new matcher implicitly starts with a fresh cache and old caches become garbage-collectable along with the matcher they belong to. (src/matcher.ts) Math.random().toString(36).substring(7)can return an empty string: occurred atsrc/router.tsx(refreshActiveRouteKey) andsrc/components/RouterWrapper.tsx(updatePage) when the base-36 representation of the random value had fewer than 7 characters. An empty Reactkeydefeats the reconciler’s mount/unmount. Replaced withgenerateRouteKey(), which composes two padded base-36 chunks and is guaranteed non-empty (6–12 chars). (src/router.tsx,src/components/RouterWrapper.tsx)LinkforwardRefgeneric order:forwardRef<LinkProps>(InnerLink) as React.FC<LinkProps>typed the first generic as the ref element (it’s actually the props slot) and the cast hid the mismatch — consumers got an incorrectly typed ref. NowforwardRef<HTMLAnchorElement, LinkProps>(InnerLink); the inner ref-forwarding callback also now handles both object refs and callback refs, where it previously assumed an object ref. (src/components/Link/Link.tsx)- Router constructor SSR crash: the singleton was instantiated at module load and called
window.addEventListener("popstate", ...)plusrouterEvents.onNavigating(...)(viasetScrollToTop("smooth")) unconditionally, makingimport "@mongez/react-router"throw under SSR/Node wherewindowis undefined. The constructor now guards both withtypeof window !== "undefined". Browser behaviour is unchanged — consumers that rely on listeners-being-active-on-import still get that. (src/router.tsx)
Added
- AI kit:
llms.txt,llms-full.txt, andskills/folder (README,overview,routes,navigation,params,lazy-loading,localization,recipes) for tool-assisted development against the public API. - README rewrite: technical reference covering every export, with the configuration table generated from the actual
RouterConfigurationstype. Removed examples that referenced non-exported helpers (e.g.getCurrentLocaleCode,getApps). - Test suite: vitest + happy-dom +
@testing-library/react. Covers route registration, dynamic segment parsing (:id,:id?,:id+,:id*), groups andpartOf, programmatic navigation (navigateTo/navigateBack/silentNavigation/refresh),<Link>rendering and click interception (incl. modifier keys,target="_blank",email/tel/ external URL passthrough), the query-string parser/serializer, the URL pattern matcher, router events, base path, and not-found handling. - CHANGELOG: this file.
- CI: GitHub Actions workflow
Testrunning Node 18/20/22 on Ubuntu, Node 20 on Windows, plus a React 19 cross-matrix entry on Node 20 / Ubuntu. vitest.config.ts: happy-dom environment, React plugin, self-detecting sibling-alias resolver. The aliases for@mongez/concat-routeand@mongez/eventsshort-circuitnode_moduleswhen their source folders exist locally (monorepo dev), and silently fall back to the published packages in CI.package.jsonmetadata:descriptionupdated from “A powerful react router system with lazy loading” to a feature-list-shaped one-liner.keywordsexpanded withrouting,lazy-loading,code-splitting,middleware,i18n,localization,prefetch,chunk-error,react-router.sideEffects: false(enables tree-shaking; the package source has no top-level side effects beyond the singleton instantiation inrouter.tsx, which consumers must opt into by importing the default export anyway).scripts.testandscripts.test:watchfor vitest.devDependenciesfor the vitest stack (vitest,happy-dom,@vitejs/plugin-react,@testing-library/react,@testing-library/dom,react,react-dom,@types/react,@types/react-dom,typescript).
router-wrapper.test.tsx: new test file covering the rebuiltRouterWrapper— subscription-lifecycle assertions (at most onerouter.renderingsubscription regardless of render count, single subscription shared across multiple wrapper instances, tear-down on unmount) and rendering-behavior assertions (initial render of the matched route, re-render in response to a"rendering"event,"rendered"event firing).generateRouteKeytests inrouter.test.ts: explicit coverage of the random-key replacement — non-empty for allMath.random()corner cases (0, tiny, near-1), stable length bounds, andrefreshActiveRouteKeyalways assigning a non-empty key.- Link
forwardReftests inlink.test.tsx: object refs and callback refs both receive the underlyingHTMLAnchorElement. - Matcher cache-invalidation test in
router.test.ts: swapping the matcher viasetMatcherproduces results from the new matcher, not from the prior matcher’s compiled cache.