Skip to content

re-frame.routing

Routes are data. You register a route with a path and a metadata map (:params, :query, :on-match, :can-leave, :can-enter, …). URL changes become events. The current route lives in the frame's runtime-db at [:rf.runtime/routing :current] and is read via the :rf/route sub. Navigation is dispatching an event.

This namespace is the public boot point and façade for the routing artefact. Requiring it wires every routing event, fx, cofx, and sub. The reg-route macro is published on the re-frame.core facade. The rest of the surface lives here:

  • URL helpers
  • registry introspection
  • scroll restoration
  • URL strategies
  • the browser URL-listener boot seam
  • the multi-frame URL-ownership resolver

For motivation and narrative, see the Routing guide.

(:require [re-frame.routing :as rf.routing])

Throughout, rf is the re-frame.core facade alias ([re-frame.core :as rf]). The reg-route macro and the route-link view live on that facade.

Route registration

reg-route

  • Kind: function (re-frame.core publishes the source-coord-capturing reg-route macro; this namespace's reg-route is the equivalent plain fn)
  • Signature:
    (reg-route id metadata path)  id
    
  • Description: Register a route as data.

    • id — keyword dispatched against later ([:rf.route/navigate {:to :route/cart}]).
    • metadata — map of match events and guards (keys below).
    • path — the URL shape; colon-prefixed segments capture into :params.

    Throws :rf.error/route-bad-metadata when metadata:

    • is not a map,
    • carries :path (the path pattern belongs in the third slot), or
    • carries a bare (unqualified) key outside the reserved set below. Namespaced keys (:myapp/analytics-id) always pass.

    Emits :rf.warning/route-shadowed-by-equal-score when an already-registered route has an equal structural rank and the two patterns can match a common URL (/a/:x vs /a/:y warns; /x/:id vs /y/:slug doesn't — they tie structurally but never compete for a URL). The earlier registration wins the tiebreak at match time, so the new route is the shadowed one: the warning's tags name it under :route-id, the existing winner under :shadowed-by, and the tied structural tuple under :rank. Emits :rf.route/registered on first-time registration.

A minimal route

(rf/reg-route :route/cart
  {:on-match [[:cart/load-items]]}
  "/cart")

Colon-prefixed path segments capture into :params. :on-match is the event vector (or vector of event vectors) dispatched when the route activates. Everything else is optional.

Reserved metadata keys

Key Notes
:doc Free-form description; pair tools read this.
:params Schemas for path segments.
:query Schemas for query-string keys.
:query-defaults Default values for query keys that are absent. Filled in wherever a target is resolved, so every door — a URL, a link, {:to …}, a prefetch — resolves the same :query. A key already at its default is not emitted into the URL (match-url fills it back), so each destination has one canonical URL.
:tags Free-form classification, e.g. #{:auth-required :admin-only :public}.
:parent Another route id. Builds a chain readable via :rf.route/chain, and composes the ancestors' :resources into this route's effective plan (parent-to-leaf, with identical requirements deduped). :parent is itself the opt-in; nothing else — :on-match, :scroll, :head, :tags, the guards — is inherited.
:on-match Event vector(s) the runtime fires and forgets when the route activates. It is not a readiness mechanism: it never moves :rf.route/transition / :rf.route/error, never awaits the async work its events start, and never rewrites their failures into route state — a throwing handler surfaces on the ordinary event error channel. Managed page reads belong in :resources.
:can-leave Guard sub-query run before leaving the route. Closed boolean contract: true allows, false blocks. Any non-boolean also blocks and emits :rf.error/can-leave-non-boolean. The name reads positively, so false means "can NOT leave". The sub receives the pending target as an argument. See Routing → Blocking a navigation.
:can-enter Guard sub-query run before entering the route (the auth-gate mirror of :can-leave). Closed boolean contract: true allows entry, false blocks. Any non-boolean also blocks and emits :rf.error/can-enter-non-boolean. A rejection is TERMINAL — nothing commits, no pending value is created, and the runtime dispatches :rf.route/entry-denied once. See Routing → Guarding entry.
:scroll Scroll-restoration behaviour for this route.
:sensitive Slice paths (projection-relative, e.g. [:query :token]) redacted at egress while the route is active. See Routing → Keeping tokens off the wire.
:large Slice paths kept off the wire at egress (a size marker ships instead of the value).

Two cross-feature bare keys are also accepted:

  • :head — SSR's head-metadata contract. It is always in the accepted set.
  • :resources — the Resources artefact's route integration, late-bound via the :routing/extra-route-keys hook. In an app without the Resources artefact, :resources is rejected like any other unknown bare key.

Guide overview of the key groups: Metadata map (per-key catalogue is this table and the API rows below).

Clearing a route

  • Signature:
    (rf/clear :route id)  id
    
  • Description: Remove a registered route. Emits :rf.route/cleared (symmetric with :rf.flow/cleared) so tools subscribing to route lifecycle observe the removal. No-op when id was not registered. There is no clear-route name: it was never a re-frame.core facade export, and re-frame.routing dropped its own re-export — :route is one of the kinds the one kind-keyed registrar inverse dispatches (see clear). re-frame.routing.registry/clear-route survives as the late-bind hook target that dispatch routes to, not as a public call (rf2-kuky.80).

reset-counters!

  • Kind: function
  • Signature:
    (reset-counters!)
    
  • Description: Test-time helper. Resets the route-registration index counter to zero so the registration-order tie-breaker in route ranking is deterministic across fixture runs.

URL and route matching

The URL ↔ route mapping is a prism. match-url reads a URL into route data. route-url renders route data back into a URL. match-url(route-url(...)) round-trips the canonical route data. Both functions are pure and JVM-runnable.

match-url

  • Kind: function
  • Signature:
    (match-url url)  {:route-id :params :query :fragment :validation-failed?} or nil
    
  • Description: Match a URL to route data. Pure; JVM-runnable.

    • Returns nil when no route matches. Also fails closed to nil on malformed percent-encoding anywhere in the URL.
    • When the route declares :params / :query schemas and the parsed values fail them, :validation-failed? is true and the explanation rides under :validation-error.
    • Query keys the route declares (via :query / :query-defaults) come back as keyword keys, in a deterministic canonical order. Undeclared keys stay strings.
    • Example:
      ;; with (rf/reg-route :user/show {} "/users/:id") registered:
      (rf.routing/match-url "/users/42")
      ;; => {:route-id :user/show, :params {:id "42"}, :query {},
      ;;     :fragment nil, :validation-failed? false}
      
      ;; nil when no route matches:
      (rf.routing/match-url "/no/such/path")  ;; => nil
      

route-url

  • Kind: function
  • Signature:
    (route-url {:to route-id :params path-params :query query-params :fragment fragment})  URL string
    
  • Description: Render a route to a URL — the inverse of match-url. Takes one address map; pure; JVM-runnable.

    • :to is the only required key (requests spell the route id :to; facts spell it :route-id). :params, :query, and :fragment are optional.
    • :fragment appends #fragment when it is a non-empty string (nil / "" append nothing).
    • Nil-valued query keys are silently elided. A nil required path param is an error.
    • A query key already at the route's declared :query-defaults value is not emittedmatch-url fills it back, so spelling it would give one destination two URLs. Validation still runs against the caller's full query.
    • Query keys are emitted percent-encoded, in a deterministic canonical order.
    • Address-only. :url, :query-merge, policy keys (:replace? / :scroll / :bypass-leave?), and any unknown key reject loud (:rf.error/route-url-validation, :reason :bad-address-keys) rather than being silently ignored. There is no in-place form — a pure helper cannot read the current route.

    Throws:

    • :rf.error/no-such-route:to route not registered.
    • :rf.error/missing-route-param — a required path segment's param is nil or absent.
    • :rf.error/route-url-validation:params / :query fail the route's :params / :query schemas, or the map carries non-address keys.
    • :rf.error/route-url-non-edn-value — non-EDN param/query values or a non-string fragment.
    • Example:
      ;; with (rf/reg-route :user/show {} "/users/:id") registered:
      (rf.routing/route-url {:to :user/show :params {:id 42}})        ;; => "/users/42"
      
      ;; query params are appended and percent-encoded:
      (rf.routing/route-url {:to :search :query {:q "hello world"}})  ;; => "/search?q=hello%20world"
      

malformed-url?

  • Kind: function
  • Signature:
    (malformed-url? url)  boolean
    
  • Description: true when any percent-encoded portion of url is malformed — a non-empty path segment, a query key or value, or the #fragment. The check is purely lexical; no route table is consulted.

    The :rf.route/handle-url-change handler uses it to tell two cases apart: a plain route miss ({:url url}) and a malformed URL that failed closed ({:url url :reason :malformed-url}). Both cases end at :rf.route/not-found. The structured :reason lets per-route error UIs and SSR projections branch on the cause.

Introspection and slice access

This is the read-side surface over the route registry and the live route slice. The live readers expose the per-frame slice.

Lowering routes into the shared derivation/process-algebra node shape — so a tool can show subscriptions, flows, resources, route facts and machine selectors as one family — is not part of it. The static route view and the live route-slice view ship no public accessor (Derivations §Routes expose algebra views): they live in re-frame.routing.tooling and every consumer names that namespace directly — Xray and the conformance fixtures statically, re-frame.derivation.graph through requiring-resolve on the JVM.

"Which routes are registered, and what is route X's spec?" has no routing-specific accessor (rf2-kuky.31 retired route-ids / route-meta). It is the generic registrar query API, which every tool already speaks:

(keys (rf/registrations {:source :store :kind :route}))
;; => (:route/cart :user/show)

(rf/handler-meta {:source :store :kind :route :id :route/cart})
;; => the registered metadata map, or nil

The returned map carries the :path pattern plus whatever the registration declared, so every reserved metadata key reads back from it — :params, :query, :query-defaults, :tags, :parent, :on-match, :can-enter, :can-leave, :scroll, :sensitive, :large, and the cross-feature :head / :resources. (:can-enter and :parent are the two an auth guard and a branch-composition read back most.) It also carries the computed :rf.route/rank / :rf.route/compiled / coercion tables and the source coords. Unlike the resource / mutation / resource-scope kinds, a route registration carries its metadata at the top level — there is no inner-key projection step.

Reading the route and the pending-nav slot

You read the route slice and the pending-navigation slot with ordinary subscribe calls naming their framework sub vectors. There is no named-read-sugar fn — a runtime-db framework read is a subscription vector, one grammar. Both are per-frame singletons, so no id argument is needed. To read an explicit frame (say, a non-default url-bound one), use subscribe's {:frame <target>} opts form.

(:route-id @(rf/subscribe [:rf/route]))   ;; the active route id, or nil pre-navigation

;; show an "unsaved changes?" prompt only while a navigation is blocked
(when-let [pending @(rf/subscribe [:rf/pending-navigation])]
  [confirm-leave-dialog pending])

The :rf.route/* granular subs (:rf.route/id, :rf.route/params, …) chain off [:rf/route].

Scroll restoration

Saved scroll positions live in a host-side, per-frame transient LRU cache keyed by frame-id. They are NOT runtime-db state. They are host-derived (read from window.scrollX/Y), meaningless server-side, and not needed to reconstitute a coherent frame on restore, SSR-hydration, or time-travel. So they never ride the trace / epoch / SSR egress wire, and they cannot rewind on an epoch restore. The pure helpers operate on a plain per-frame cache map {:positions {url [x y]} :order [url ...]}. The !-suffixed wrappers read and write the host cache.

scroll-positions-cap

  • Kind: value
  • Signature:
    scroll-positions-cap  ;; => 50
    
  • Description: Soft upper bound on tracked URLs in the per-frame scroll-positions cache. It is large enough that real Back-button restoration hits saved positions, and small enough that the per-frame host cache stays bounded over long sessions.

frame-scroll-cache

  • Kind: function
  • Signature:
    (frame-scroll-cache frame-id)  {:positions :order} or nil
    
  • Description: Read the per-frame cache map ({:positions :order}) for frame-id from the host scroll-position cache, or nil when none. This is the value threaded into the pure nav-planning seam.

lookup-scroll-position

  • Kind: function
  • Signature:
    (lookup-scroll-position cache url)  [x y] or nil
    
  • Description: Pure. Return the saved [x y] for url in cache, or nil if none is saved. cache is a per-frame cache map {:positions {url [x y]} :order [...]}; it may itself be nil.

save-scroll-position

  • Kind: function
  • Signature:
    (save-scroll-position cache url xy)  cache'
    
  • Description: Pure. Return cache with the scroll position for url recorded under :positions. The cache is LRU-capped at scroll-positions-cap. Re-saving an existing url promotes it to most-recent. A new save past the cap evicts the least-recently-used entry. The :order vector is the recency anchor.

save-scroll-position!

  • Kind: function
  • Signature:
    (save-scroll-position! frame-id url xy)  nil
    
  • Description: Record xy for url under frame-id in the host scroll-position cache, applying the LRU cap via the pure save-scroll-position.

reset-scroll-cache!

  • Kind: function
  • Signature:
    (reset-scroll-cache!)  nil
    
  • Description: Test-time helper. Drop the whole host scroll-position cache so a saved position does not leak across tests.

The nav-token and pending-nav allocators are host-side, per-frame, monotonic high-water marks — not runtime-db state. An epoch restore replaces the runtime-db partition wholesale, so it cannot rewind these counters and recycle a token still carried by a slow in-flight continuation. counter-snapshot reads them. routing-state-classification is the canonical durable/transient map that SSR, docs, and schemas key off.

counter-snapshot

  • Kind: function
  • Signature:
    (counter-snapshot frame-id)  {:nav-token-counter N :pending-nav-counter M} or {}
    
  • Description: Read the per-frame counter snapshot for frame-id from the host nav-counters cache, or {} when none. This is the value the allocation-cofx generators mint the next nav-token / pending-nav id from.

routing-state-classification

  • Kind: value
  • Signature:
    routing-state-classification
    ;; => {:durable-runtime-db             {:keys [:current] :doc "..."}
    ;;     :local-subscribable-runtime-db  {:keys [:pending-navigation] :doc "..."}
    ;;     :host-transient                 {:keys [:scroll-positions
    ;;                                             :nav-token-counter
    ;;                                             :pending-nav-counter] :doc "..."}}
    
  • Description: The canonical classification of every piece of per-frame routing state, by tier. Consumed by SSR, docs, and Spec-Schemas so the durable/transient split has one home.

    • :durable-runtime-db — serializable facts needed to reconstitute a coherent frame on restore / SSR-hydration; the route slice at :current.
    • :local-subscribable-runtime-db — runtime-db state that stays subscribable and restores in local replay but is SSR-stripped fail-closed; the :pending-navigation slot.
    • :host-transient — host-derived caches never in runtime-db; saved scroll positions and the two allocator high-water marks.

reset-nav-counters!

  • Kind: function
  • Signature:
    (reset-nav-counters!)  nil
    
  • Description: Test-time helper. Drop the whole host nav-counters cache so a counter value does not leak across tests. Wired into the shared reset-runtime fixture via the :routing/reset-nav-counters! late-bind key.

Multi-frame URL ownership

At most one frame owns the browser URL at a time. A frame claims ownership by registering with {:url-bound? true}. The resolver below names the current owner. The outbound :rf.nav/push-url fx and the inbound popstate listener both route through it — one owner, both directions.

url-owner-frame-id

  • Kind: function
  • Signature:
    (url-owner-frame-id)  frame-id or nil
    
  • Description: Return the single frame that has explicitly declared browser-history ownership via (rf/make-frame {:id … :url-bound? true}), or nil when none has.

    • URL ownership is an explicit host/bootstrap policy, not an absence repair. The runtime never infers :rf/default as the owner. :rf/default owns the URL only when it carries an explicit {:url-bound? true}, like any other frame.
    • Ownership resolves to the first-claimed still-live :url-bound? true frame (the incumbent), so a later duplicate cannot steal the URL.
    • nil means no owner is declared. In that case outbound history fxs no-op and the inbound popstate listener skips.
    • Example:
      ;; one frame opts into URL ownership at boot:
      (rf/make-frame {:id :app/main :url-bound? true})
      (rf.routing/url-owner-frame-id)  ;; => :app/main
      

reset-url-claims!

  • Kind: function
  • Signature:
    (reset-url-claims!)  nil
    
  • Description: Test-time helper. Drop the whole URL-ownership claim-order vector so a prior test's URL claim does not leak into the next. Wired into the shared reset-runtime fixture via the :routing/reset-url-claims! late-bind key.

URL strategies

A :url-strategy is a frame-level config map declared on the URL-owning frame — (rf/make-frame {:id :app :url-bound? true :url-strategy rf.routing/hash-url-strategy}). The strategy is consulted at exactly four egress/ingress points: the two history fxs, the route-link href render, and the URL-listener install. route-url, match-url, and the navigation cascade stay pure and path-form. A strategy map carries {:encode :decode :push! :replace! :install-listener!}. The side-effecting keys (:push! / :replace! / :install-listener!) are present on CLJS only. SSR runs none of them — the server takes the request URL via :rf.route/handle-url-change and drives no history — but it does honour the pure :encode: a server-rendered route-link carries the same encoded href the hydrated client renders (/demos/active for a with-base-path frame, #/active for a hash frame).

history-url-strategy

  • Kind: value
  • Signature:
    history-url-strategy  ;; {:encode :decode :push! :replace! :install-listener!}
    
  • Description: The default URL strategy: HTML5 History, path-form. A frame that declares no :url-strategy uses this.

    • :encode / :decode are identity over the app-relative URL (:decode reads pathname + search + hash).
    • :push! / :replace! drive pushState / replaceState.
    • :install-listener! wires popstate.

hash-url-strategy

  • Kind: value
  • Signature:
    hash-url-strategy  ;; {:encode :decode :push! :replace! :install-listener!}
    
  • Description: The hash URL strategy: #-prefixed URLs (#/active) for no-server-rewrite static hosting and secretary-era v1 migrations. route-url still builds path-form /active.

    • :encode maps it to #/active at the route-link href and the history fxs.
    • :decode strips the leading # from window.location.hash (an empty hash decodes to /).
    • :install-listener! wires hashchange.
    • Example:
      (rf/make-frame {:id :app
                      :url-bound?   true
                      :url-strategy rf.routing/hash-url-strategy})
      

with-base-path

  • Kind: function
  • Signature:
    (with-base-path strategy base)  strategy-map
    
  • Description: A STRATEGY COMBINATOR, not a third shipped strategy. Use it when an app is deployed under a sub-path — say a host mounting several demos side by side, so an app that would otherwise own / instead lives at /realworld/. It wraps strategy (either shipped strategy, or a custom one) so that :encode / :decode / :push! / :replace! / :install-listener! all account for base. The base is stripped off every inbound URL and re-added to every outbound one, underneath whichever address-bar form strategy already provides. route-url / match-url and the rest of the cascade stay path-form and base-agnostic. A blank or nil base returns strategy unchanged.
  • Example:
    (rf/make-frame {:id :app
                    :url-bound?   true
                    :url-strategy (rf.routing/with-base-path
                                    rf.routing/history-url-strategy
                                    "/realworld")})
    

Browser URL listener

There is no imperative boot seam to call. The browser popstate / hashchange listener is wired automatically by the :url-bound? frame LIFECYCLE (rf2-g8pbwg). When a :url-bound? true frame is created (or re-registered) and resolves as the URL owner, that step installs the listener AND syncs the current URL into the owner's route slice. Destroying the frame removes the listener. The retired install-url-listener! / remove-url-listener! / install-history-listener! / remove-history-listener! exports are GONE (pre-alpha, no back-compat shim). There is nothing to call.

  • Each browser-driven change is decoded to a path-form URL by the strategy's :decode, then dispatched synchronously as :rf.route/handle-url-change to (url-owner-frame-id), resolved at fire time. When no frame declares :url-bound? true, the dispatch is skipped.
  • The listener kind (popstate vs hashchange) is resolved from the URL-owning frame's :url-strategy at install time. The owner (the dispatch target) is re-resolved at every fire.
  • Installation is idempotent. A re-registration that resolves as the owner tears down the prior listener before reinstalling. A losing duplicate :url-bound? true registration never installs.
  • CLJS-only. On the JVM there is nothing to install; SSR feeds the request URL via :rf.route/handle-url-change.

The :route/link registered view renders an <a href=...> from a route id. It intercepts plain left-clicks and turns them into :rf.route/url-requested dispatches. The authoring surface is also published as rf/route-link on the re-frame.core facade.

  • Kind: component (the registered :route/link view). There is no re-frame.routing/route-link var — the authoring name is rf/route-link on the re-frame.core facade, which reaches the view through routing's :routing/route-link late-bind hook.
  • Signature:
    [route-link {:to :route-id :params {...} :query {...} :fragment "..."
                 :prefetch :intent :on-click f & html-attrs}
     & children]
    
  • Description: The registered :route/link view.

    • :to is the only required key. :params, :query, and :fragment are forwarded to route-url for href synthesis. :prefetch is the one behaviour key (below). Every other props key passes through to the <a> element — including :aria-current and :class, which is how an "active link" is styled: route-link computes no active state, so compare :to against :rf.route/id (or :rf.route/chain) in your own view. See Routing → Highlighting the active link.
    • A plain primary-button click (no modifier keys, defaultPrevented false) is intercepted. The view calls preventDefault, then dispatches [:rf.route/url-requested {:url ...}] targeted at the frame that rendered the link. One key: a raw URL is the whole address, and the handler re-derives the route from it.
    • Modifier-key / middle-button clicks, and anchors carrying native-handling attributes (:target other than _self, or :download), defer to the browser.
    • A caller-supplied :on-click runs first. If it calls preventDefault, the framework's interception is skipped.
    • :prefetch :intent warms the destination on hover, focus, or touch by dispatching :rf.route/prefetch with the link's own address (:fragment excluded — a fragment is never a resource input). :intent is the only accepted value, and omitting :prefetch is the only way to opt out — a key present with any other value (including true, false, nil, or a mode borrowed from another router) throws :rf.error/route-link-bad-prefetch at the render site rather than quietly rendering a passive link. Caller-supplied :on-mouse-enter / :on-focus / :on-touch-start handlers still run — the framework composes rather than replaces. The intent handlers are CLJS-only (SSR renders the anchor with none), but the value is validated on both hosts, so the server shell never accepts a mode the hydrated client rejects. re-frame.fresco/route-link honours the same key from the same calculation, reached through the :routing/link-model seam; because Fresco's grammar carries one intent per position it refuses a caller value at a claimed position rather than composing — see Fresco → Routing and navigation.
    • The rendered href is encoded through the rendering frame's :url-strategy — on both hosts, so the server shell and the hydrated client agree.
    • On the JVM the :route/link registration renders via route-link-render-ssr.
    • Example:
      [rf/route-link {:to :user/show :params {:id 42} :class "nav-item"}
       "Profile"]
      

Server-side rendering

  • Kind: function
  • Signature:
    (route-link-render-ssr props & children)  hiccup
    
  • Description: The JVM render fn for the :route/link view. It renders the <a href=...> shell without the click-interception logic. SSR has no DOM events to intercept, so the anchor is emitted as-is; clicks on the hydrated page run the CLJS render fn's on-click path. The href is encoded through the rendering frame's :url-strategy exactly as on the client — the SSR pipeline pins the request frame around its render walk, so a with-base-path server frame emits /demos/active and a hash frame #/active, matching the hydrated render; called outside any frame scope it renders the path form (the history default). The authoring surface is route-link, also published on the re-frame.core facade.

Keyword surfaces

The routing artefact registers a family of events, subscriptions, effects, and coeffects addressed by keyword. Loading re-frame.routing wires them all. It also registers internal machinery that apps and tools never dispatch or declare: the :rf.route.internal/* runtime events, the recordable allocation cofx (:rf.route/nav-allocation, :rf.route/pending-nav-allocation), and the :rf.route/commit-nav-counter fx. Those internals are omitted from the tables below.

Events

Standard events the runtime dispatches (or you dispatch) around routing.

Event Notes
:rf.route/navigate Navigate via one request map: [:rf.route/navigate {request}]. Address keys :to (route id) / :url (raw-URL escape hatch) / :params / :query / :fragment; policy keys :replace? / :scroll / :bypass-leave?; edit key :query-merge. :to xor :url; :url excludes :params / :query / :query-merge. Omit both :to and :url for an in-place request that patches the current location (:query-merge, or a :query / :fragment present by key). A structurally-invalid request rejects loud with :rf.error/navigate-bad-request before any guard runs.
:rf.route/handle-url-change The one URL-change handler, for a link click / popstate / initial load / SSR. The cause rides :rf.route/cause on the trailing opts map (:link / :popstate / :initial / :ssr); an omitted rider resolves to :initial on a client frame and :ssr on a :platform :server one. Default scroll is :top for :link and :restore for every other cause. The runtime dispatches this; you read it, and you may override it for custom URL-change handling.
:rf.route/url-requested The user clicked a framework-owned link. route-link synthesises this event; you usually let the default handler take it.
:rf.route/navigation-blocked A :can-leave guard rejected a navigation. The pending-nav slot carries the rejected attempt as {:id :destination :target :cause :policy :requested-url :rejecting-route :rejecting-guard :url-restored?}. The slot is leave-only — no direction discriminator, because there is only one thing it can be.
:rf.route/entry-denied A :can-enter guard rejected a navigation. Terminal — nothing commits and no pending value is created; dispatched exactly once per attempt with {:destination :target :cause :requested-url :guard}. The natural place to redirect (e.g. to login); a framework no-op default ships, so registering one is optional.
:rf.route/continue User-dispatched event proceeding a blocked navigation — "yes, leave the page." Replays the pending value's :destination and :policy through the normal pipeline with a one-shot :bypass-leave? true, so the target's :can-enter is still evaluated. Event vector: [:rf.route/continue pending-nav-id].
:rf.route/cancel User-dispatched event abandoning a blocked navigation — "stay here, drop the pending nav." Event vector: [:rf.route/cancel pending-nav-id].
:rf.route/prefetch Warm a destination's effective resource plan without navigating: [:rf.route/prefetch {:to :route/article :params {:slug "x"}}]. Accepts a named address only (never :url); an invalid one rejects before planning with :rf.error/prefetch-bad-address. Runs the same parent-to-leaf plan a navigation would, in warm mode — every ensure ownerless, :blocking? inert, no route state, no guards, no :on-match. Frame-scoped, and a no-op beyond its :rf.route/prefetched summary trace when the resources artefact is absent or the plan is empty. [route-link {… :prefetch :intent}] dispatches it for you on hover / focus / touch.
:rf.route/replan-resources Rerun the active route's effective resource plan against the current app-db without navigating: [:rf.route/replan-resources {:cause [:session-restore]}]. The one causal door for an identity input (principal, tenant, locale) that changed with no route change — a {:from-db …} subscription re-keys passively and sits :idle until something ensures the new key. Same token, same owner, same planner: kept identities are adopted with no fetch, added ones are ensured under the route owner with your :cause, dropped ones lose the owner, the durable plan / blocking facts are replaced and readiness is re-projected — so a successful replan clears an earlier :rf.error/resource-route-plan. A planning failure is a committed failed replan (no partial ensure, the owner released from every prior identity). :cause is required and non-nil; a malformed payload or a dispatch with no active route rejects before planning with :rf.error/replan-bad-request. Not a reload: unchanged usable data is never refetched, and no guards, :on-match, URL, history or scroll work runs. A no-op when the resources artefact is absent.

Subscriptions

The full :rf/route slice is {:route-id :params :query :fragment :transition :error :nav-token}. The standard subs are projections of that slice plus a couple of conveniences.

Sub Returns
:rf/route The full :rf/route slice {:route-id :params :query :fragment :transition :error :nav-token}. Read with @(rf/subscribe [:rf/route]).
:rf.route/id Current route id (the slice's :route-id)
:rf.route/params Current path params
:rf.route/query Current query params
:rf.route/transition :idle / :loading / :error — a projection over the blocking :resources in the effective route plan. :loading while a blocking first load is pending; :error on a blocking first-load failure or a plan that could not be built; :idle otherwise (and always, with no resources artefact loaded). A background refresh, a non-blocking read, an intent prefetch, and :on-match never move it.
:rf.route/error The structured failure when :transition is :error:rf.error/resource-route-blocking for a blocking first-load failure, :rf.error/resource-route-plan for a plan that could not be built; nil otherwise
:rf.route/fragment Current URL fragment (string or nil)
:rf.route/chain Vector of route ids from parent-most to current (per :parent links)
:rf/pending-navigation The pending-nav slot (per :rf/pending-navigation schema) when a :can-leave guard has parked a navigation; nil otherwise. Leave-only — a denied entry is terminal and parks nothing. Read with @(rf/subscribe [:rf/pending-navigation]).

Effects (fx)

Fx Args Platforms Notes
[:rf.nav/push-url url-string] URL string :client Push a new URL onto the browser history.
[:rf.nav/replace-url url-string] URL string :client Replace the current URL without adding a history entry.
[:rf.nav/scroll scroll-spec] scroll-spec map :client Restore or set scroll position.
[:rf.nav/capture-scroll {:url url-string}] {:url ...} map :client Capture the current scroll position into the host-side per-frame scroll-position cache (keyed by url) before leaving a route.
[:rf.route/with-nav-token {:rf/reply-to <reply-target> :nav-token <token>}] see notes universal Name an async-completion continuation by its canonical :rf/reply-to reply target and guard it with a navigation token. On a token match, the target is completed with the :status :ok reply map. If the token has been superseded by a later navigation, the completion is suppressed and :rf.route.nav-token/stale-suppressed fires. Optional args keys: :route-id (the captured route id, for the work-id), :value (rides the :status :ok reply map), :completed-at.

The nav-token wrapper guards against "user navigates away mid-load". The older load's reply carries the stale token. The runtime suppresses it, so the older page's data does not overwrite the newer page's state. Full semantics in Routing → Activation work and page data.

Coeffects (cofx)

Declare these on a handler via :rf.cofx/requires. Each value is delivered flat under the cofx key in the coeffects map. Both are universal (client and server).

Cofx Delivers
:rf.route/nav-token The current navigation epoch token, read from [:rf.runtime/routing :current :nav-token]. Declare {:rf.cofx/requires [:rf.route/nav-token]} on an :on-match-reached handler to capture the epoch live at scheduling time and thread it into an async continuation. :rf.route/with-nav-token validates it on receipt.
:rf.route/route-id The current route id, read from [:rf.runtime/routing :current :route-id]. The capture-side companion of :rf.route/nav-token. Declare both ({:rf.cofx/requires [:rf.route/nav-token :rf.route/route-id]}) so the route-loader work-id [:rf.work/route route-id nav-token loader-id] carries its complete attempt identity.

See also

  • re-frame.core.md — the re-frame.core facade: the reg-route macro's brief row and the route-link view. The browser URL-change listener is NOT on the facade — it is wired automatically by the :url-bound? frame lifecycle (see Browser URL listener above).
  • re-frame.ssr.md — routes participate in SSR; the active route's :head registration is what head-model looks up.
  • Routing guide — the narrative side: a tutorial, concepts (nav-token semantics, :can-leave flows, query strings, multi-frame routing), and how-to recipes.
  • Routing glossary — the surface vocabulary (navigate, route, loader, route guard, not-found, url-bound?).
  • Coming from React Router — the mapping, and where re-frame2 routing diverges.