re-frame.routing¶
Routing maps URLs to named routes and back. You register each route as data, with an id, a metadata map and a path. Navigating is dispatching an event, and the active route is a subscription: it lives in the frame's runtime-db at [:rf.runtime/routing :current] and is read with [:rf/route].
Routing ships in the optional day8/re-frame2-routing artefact. Require re-frame.routing once at boot; loading it registers every routing event, effect, coeffect and subscription, and the :route/link view. Without it, rf/reg-route, rf/route-link and (rf/clear :route id) throw :rf.error/routing-artefact-missing.
(rf/reg-route :app/home {} "/")
(rf/reg-route :app/article {} "/articles/:id")
(rf/reg-view root-view []
(case @(subscribe [:rf.route/id])
:app/home [rf/route-link {:to :app/article :params {:id "intro"}} "Read intro"]
:app/article [:h1 "Article " (:id @(subscribe [:rf.route/params]))]
[:h1 "Not found"]))
;; Navigate from an event handler.
(rf/reg-event :article/open
(fn [_ [_ id]]
{:fx [[:dispatch [:rf.route/navigate {:to :app/article :params {:id id}}]]]}))
;; At the render root: this frame owns the browser's address bar.
[rf/frame-root {:id :app/main :url-bound? true} [root-view]]
reg-route and route-link are called on the re-frame.core facade. The URL helpers, URL strategies and test hooks are called on this namespace as rf.routing/…, and events, effects and subscriptions are addressed by keyword. Inside reg-view, subscribe and dispatch are bound without the rf/ prefix.
The frame created with :url-bound? true owns the browser's address bar. Its navigations push browser history entries, Back and Forward navigate it, and on creation it reads the current URL, so a deep link or a reload lands on the right route. Leave the flag off for a frame that routes in memory only, such as a story, a test fixture or an embedded widget: its route slice still changes, but the address bar does not. See Multi-frame URL ownership.
The model teaches routes, navigation, guards and page data.
Route registration¶
reg-route¶
- Kind: macro (
rf/reg-route); also a function,rf.routing/reg-route, which does not capture source coordinates - Signature:
- Description: Registers a route.
idis the keyword you navigate to ([:rf.route/navigate {:to :route/cart}]),metadatadeclares its match events, guards and schemas (keys below), andpathis its URL pattern.- Emits
:rf.warning/route-shadowed-by-equal-scorewhen an existing route has an equal structural rank and the two patterns can match a common URL./a/:xand/a/:ywarn;/x/:idand/y/:slugtie in rank but never match the same URL, so they do not. The earlier registration wins 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 the
:rf.route/registeredtrace the first time an id is registered.
- Emits
-
Path patterns:
pathis built from these parts. Trailing slashes are ignored when matching, and matching is case-sensitive.Part Example Matches Literal segment /articlesThat segment exactly. Named param /:idOne segment, captured into :paramsas a string unless the:paramsschema coerces it.Optional group /articles/:id{/:slug}?,{/:lang}?/aboutThe group or nothing. The slash goes inside the braces. Splat /files/*restOne or more segments, captured as one string such as "a/b.txt". At most one, and it must come last.Bare /*/*Every URL, including /. A match-only fallback; use a concrete route for URL generation.Param names are bare identifiers (
:id, not::app/id). Optional groups contain slash-prefixed literals or params; they cannot nest or contain a splat. Their inner segments do not count toward the literal or segment counts below. Percent-encode reserved pattern characters (:,*,{,},?) when they are literal text.When several routes match a URL, the most specific wins, compared in this order:
- more literal segments
- any route over the bare
/* - more segments
- a named param over a splat
- a route without an optional group
- the earlier registration
-
Options (the
metadatamap; every key is optional):Key Notes :docFree-form description, read by tools. :paramsA Malli [:map …]schema for the path params. Captured strings are coerced to the declared type for:int,:uuid,:booleanand keyword[:enum …]slots; other types stay strings. When the schemas artefact is loaded the values are also validated, in every build: a URL that fails lands on:rf.route/not-foundwith:reason :validation, and a{:to …}navigation that fails is rejected.:doubleand bare:keywordslots are refused at registration, because they cannot round-trip through a URL.:queryA Malli [:map …]schema for query-string keys, coerced and validated as for:params. Keys declared here or in:query-defaultscome back as keywords; value coercion follows this schema, not the type of a default. Any undeclared key stays a string key with a string value.:query-defaultsDefault values for absent query keys, filled in wherever a target is resolved, so a URL, a link, {:to …}and a prefetch all resolve the same:query. A key already at its default is left out of the URL andmatch-urlfills it back, so each destination has one canonical URL.:tagsFree-form classification, e.g. #{:auth-required :admin-only :public}.:parentAnother route id. Builds the chain read by :rf.route/chain, and adds the ancestors':resourcesto this route's plan, parent to leaf, with identical requirements deduplicated. Nothing else is inherited: not:on-match,:scroll,:head,:tagsor the guards.:on-matchA vector of event vectors, dispatched in order each time a navigation commits this route with a new route id, params or query; an identical or fragment-only navigation does not re-fire it. A handler reads the new route from its :rf.db/runtimecoeffect at[:rf.runtime/routing :current]. It only dispatches: it never moves:rf.route/transitionor:rf.route/error, never waits for the work its events start, and never turns their failures into route state. A throwing handler reports through the ordinary event error channel. Use it for work such as analytics or seeding UI state, and declare a page's data in:resources. A single event is written[[:app/load]];[:app/load]is refused at registration.:can-leaveA subscription id or query vector, subscribed before leaving the route with the pending target {:route-id :params :query :fragment :url}appended as its last element.trueallows the navigation andfalseblocks it. Any other value also blocks, and emits:rf.error/can-leave-non-boolean. See Routing → Blocking a navigation.:can-enterThe same shape, subscribed before entering the route, such as an auth check. trueallows entry andfalseblocks it. Any other value also blocks, and emits:rf.error/can-enter-non-boolean. A rejection is final: nothing commits, no pending navigation is created, and the runtime dispatches:rf.route/entry-deniedonce. See Routing → Guarding entry.:scrollWhere the page scrolls on entering the route: :top(to the element named by the#fragment, or the top of the page),:restore(the position saved for this URL),:preserve(no movement), orfalse(no scroll effect). Without it, a link click or:rf.route/navigateuses:top, and Back, Forward and the initial load use:restore. A:rf.route/navigaterequest's own:scrolloverrides the route's. Any other value is rejected by the:rf.nav/scrolleffect with:rf.error/unsupported-scroll-strategy.:resourcesThe page's server data, from the Resources artefact; without that artefact the key is rejected like any unknown key. A vector of requirement maps, with the keys in the table below. Entering the route ensures each resource with the route as owner, and leaving releases it. See Routing → Declaring the data a page needs. :sensitiveSlice paths, relative to the route projection (e.g. [:query :token]), redacted at egress while the route is active. A[:query k]path matches only whenkis declared in:queryor:query-defaults; otherwise registration warns with:rf.warning/route-classification-query-key-unpromoted. See Routing → Keeping tokens off the wire.:largeSlice paths replaced by a size marker at egress, so the value itself is not sent. :headSSR's head metadata, accepted unqualified whether or not the SSR artefact is loaded. See re-frame.ssr.head. :nsThe namespace an image's :select-nsmatches this registration against. It is not a routing key; set it on a programmaticreg-routeto make the route selectable.Each
:resourcesentry:Key Notes :resourceThe resource id. :params(fn [route] params). Omit it for a resource that takes none.:blocking?truekeeps:rf.route/transitionat:loadinguntil the first load settles. It is also the SSR wait point.:keep-previous?Keeps the previous params' data readable while the next params load. :when(fn [route ctx] bool). Includes the entry conditionally.:scopeOverrides the resource's registered scope. :id,:afterA local id for the entry, and :after #{id}to order the ensures.The guide groups these keys by purpose in Metadata keys. - Errors: -
:rf.error/route-bad-metadata:metadatais not a map, carries:path(the pattern goes in the third argument), carries an unqualified key outside the set above, or has an:on-matchthat is not a vector of event vectors (the error names the key under:keysand carries the value under:value). Namespaced keys such as:myapp/analytics-idare always accepted. -:rf.error/invalid-route-pattern:pathbreaks the pattern grammar, for example a missing leading/, an empty segment, a splat that is not last, or an optional group whose slash is outside the braces (/{:lang}?/about). -:rf.error/route-decimal-unsupported: a:paramsor:queryslot is:double. Encode the value as a string, or use:int. -:rf.error/route-keyword-unbounded-unsupported: a:paramsor:queryslot is a bare:keyword. Use a keyword[:enum …], or:string. -:rf.error/invalid-route-classification: a:sensitiveor:largepath is malformed. - Example:;; "/articles/42" matches with :params {:id 42}. (rf/reg-route :app/article {:params [:map [:id :int]]} "/articles/:id") ;; A typed query with a default, and an entry guard. (rf/reg-sub :auth/signed-in? (fn [db _] (some? (:user db)))) (rf/reg-route :app/search {:query [:map [:q {:optional true} :string] [:page {:optional true} :int]] :query-defaults {:page 1} :can-enter [:auth/signed-in?]} "/search") ;; Page data through the Resources artefact (:cart/items is a registered resource). (rf/reg-route :app/cart {:resources [{:resource :cart/items :blocking? true}]} "/cart")
Not-found route¶
Register a route under the reserved id :rf.route/not-found to render a page for URLs that do not resolve. Its path is only a placeholder. A link click, Back or Forward, the initial load or SSR commits it for any URL that does not resolve to a route, and so does [:rf.route/navigate {:url …}] for a URL no route matches. See Routing → Not found is a route you register. The requested URL is in :params:
:params |
Cause |
|---|---|
{:url url} |
No route matched. |
{:url url :reason :validation} |
A pattern matched, but the route's :params or :query schema rejected the values. |
{:url url :reason :malformed-url} |
The URL has malformed percent-encoding. |
{:url url :reason :match-error} |
Matching the URL threw. |
(rf/reg-route :rf.route/not-found {} "/404")
;; Rendered from the root view's case as :rf.route/not-found [not-found-page]
(rf/reg-view not-found-page []
[:h1 "No page at " (:url @(subscribe [:rf.route/params]))])
- A URL-driven miss also reports
:rf.error/no-such-handler(:kind :route) on the always-on:errorsstream, which SSR answers with a 404. - A malformed URL, or one whose matching threw, also emits
:rf.warning/malformed-urlon the development trace stream, from a URL-driven change and from[:rf.route/navigate {:url …}]alike. Its tags carry the URL under:url, with query and fragment values redacted, and:reason :match-errorwhen matching threw. - When no
:rf.route/not-foundroute is registered, the slice still takes that id and the runtime emits:rf.warning/no-not-found-route. - A
{:to …}navigation never lands here: an unregistered route or a failing param rejects it instead (see:rf.route/navigate).
Clearing a route¶
- Signature:
- Description: Removes a registered route and emits the
:rf.route/clearedtrace, so tools that follow route registrations see the removal. Does nothing whenidis not registered. There is noclear-routefunction on either namespace;:routeis one of the kindsclearremoves.
Route links¶
route-link¶
- Kind: component (
rf/route-link, the registered:route/linkview; there is nore-frame.routing/route-linkvar) - Signature:
- Description: Renders an
<a href=...>for a route and turns a plain left-click into navigation.:tois the only required key.:params,:queryand:fragmentare passed toroute-urlto build the href.:replace?,:scrolland:bypass-leave?apply to the navigation the click makes, as they do on:rf.route/navigate. Every other key except:prefetchpasses through to the<a>, including:classand:aria-current.route-linkcomputes no active state: to style the active link, compare:towith: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,
defaultPreventedfalse) is intercepted: the view callspreventDefault, then dispatches[:rf.route/url-requested {:url ...}]to the frame that rendered the link, with any of the link's:replace?,:scrolland:bypass-leave?beside:url. The URL is the whole address, and the handler derives the route from it. - Modifier-key and middle-button clicks, and anchors with
:targetother than_selfor with:download, are left to the browser. - A caller-supplied
:on-clickruns first. If it callspreventDefault, the link does not intercept the click. :prefetch :intentdispatches:rf.route/prefetchwith the link's own address on hover, focus or touch. The address leaves out:fragment, which is never a resource input. Caller-supplied:on-mouse-enter,:on-focusand:on-touch-starthandlers still run alongside.:intentis the only accepted:prefetchvalue, and leaving the key out is the only way to opt out. The intent handlers exist only in ClojureScript (SSR renders the anchor without them), but the value is validated on both hosts, so the server never accepts a value the client rejects.re-frame.fresco/route-linkaccepts the same:prefetchkey, but throws:rf.error/fresco-route-link-claimed-intent-positionif the link also supplies one of those three handlers. See Fresco → Prefetch on user intent.- The href is encoded through the rendering frame's
:url-strategyon both hosts, so the server-rendered link and the hydrated client agree. On the JVM the view renders withroute-link-render-ssr.
- Errors: each is thrown at the render site, in this order.
:rf.error/no-frame-context: in ClojureScript, the link renders outside any frame. It captures its frame at render so the click dispatches there, so render it under aframe-rootorframe-provider. On the JVM the link reads the frame without requiring one, and outside any frame it renders the history-strategy href.:rf.error/route-link-bad-prefetch::prefetchis present with any value other than:intent, includingtrue,falseandnil. It is checked before the route lookup, so a mistyped:todoes not hide it.route-url's errors, from building the href::rf.error/no-such-route,:rf.error/missing-route-param,:rf.error/route-url-validationand:rf.error/route-url-non-edn-value. Seeroute-url.
- Example:
Events¶
Loading re-frame.routing registers these events, and the subscriptions, effects and coeffects in the next three sections. It also registers some internal ones that apps and tools never use directly, which this page leaves out: the :rf.route/nav-allocation and :rf.route/pending-nav-allocation coeffects and the :rf.route/commit-nav-counter effect. The :rf.route.internal/* event namespace is reserved for the runtime and has no members.
Applications dispatch :rf.route/navigate, :rf.route/continue, :rf.route/cancel, :rf.route/prefetch and :rf.route/replan-resources. route-link dispatches :rf.route/url-requested, and the runtime dispatches the other three; an SSR app also dispatches :rf.route/handle-url-change with the request URL.
:rf.route/navigation-blocked and :rf.route/entry-denied are the two events an application registers its own handler for. Registering a handler under any other routing event id makes frame creation throw :rf.error/image-duplicate-id.
[:rf.route/navigate {request}]¶
- Kind: event
- Payload: one request map. Address keys:
:to(a route id),:url(a URL inside the app),:params,:query,:fragment. Policy keys::replace?,:scroll,:bypass-leave?. Edit key::query-merge. - Description: Navigates the frame the event is dispatched to.
- With
:to, the target is built from:params,:queryand:fragmentalone, and the route's:query-defaultsfill absent query keys. Nothing carries over from the current route. - With
:url, the URL is matched as a link click's would be, which suits a URL from a notification or a server redirect.:fragmentbeside it replaces the URL's own. A URL no route matches commits the not-found route. An external URL is never followed: the request does nothing and emits the:rf.route/external-url-requestedtrace. - With neither, the request edits the current location in place.
:queryreplaces the whole query,:query-mergemerges into it (anilvalue removes that key), and:fragmentreplaces the fragment. Path params cannot change in place, because new params are a new destination. :replace? truereplaces the current history entry instead of pushing one.:scrolloverrides the target route's:scrollfor this navigation.:bypass-leave? trueskips the current route's:can-leaveguard once; the target's:can-enterstill runs.- A request identical to the current location does nothing and runs no guards. Any other request runs the current route's
:can-leaveguard, then the target's:can-enter, and afalsefrom either ends it (see:rf.route/navigation-blockedand:rf.route/entry-denied). - A request that changes only the fragment then updates
:fragment, pushes the URL and scrolls. It keeps the navigation token and does not re-run:on-matchor the resource plan. - Any other allowed request commits: the route slice is written, the URL pushed or replaced,
:on-matchdispatched, the resource plan run and the scroll applied. If resource planning fails, the route commits with:transition :error, no partial resource ensures run, and:on-matchis skipped.
- With
-
Errors: both leave the route slice unchanged and push nothing.
:rf.error/schema-validation-failure(:where :event):route-urlcannot build the target, because:tois not registered, a path param is missing, or the route's schemas reject the params or query.route-url's error is under:error, elided when the route's schema marks a slot:sensitive?.:rf.error/navigate-bad-request: the request breaks one of the rules below, checked before any guard runs.:reasonnames the rule, and:keysthe offending keys.
[:rf.route/url-requested {:url url}]¶
- Kind: event
- Payload:
{:url url}, plus any of the policy keys:replace?,:scrolland:bypass-leave?, with the meanings they have on:rf.route/navigate. - Description: Dispatched for a click on a
route-link, with the link's URL. A document-level click listener of your own can dispatch it too; see Routing → Linking from views.- An external URL does nothing here and emits the
:rf.route/external-url-requestedtrace. - A URL that resolves to the current location does nothing and runs no guards.
- Otherwise the guards run before the address bar moves, so a blocked or denied click adds no history entry. When both allow, the handler pushes the URL (or replaces it, for
:replace? true) and dispatches:rf.route/handle-url-changewith cause:link, which commits the route.
- An external URL does nothing here and emits the
[:rf.route/handle-url-change url opts?]¶
- Kind: event
- Payload:
url, an app URL such as"/articles/intro?tab=comments", and an optional opts map. - Description: Commits the route for a URL the address bar already shows. The runtime dispatches it after a link click, on Back and Forward, and for the current URL when a
:url-bound? trueframe takes ownership. On the server, dispatch it with the request URL, as in SSR → Reading the request.- The opts map's
:rf.route/causesays why the URL changed::link,:popstate,:initialor:ssr. The runtime sets it on its own dispatches. Without it the cause is:ssron a:platform :serverframe and:initialotherwise. Scroll defaults to:topfor:linkand:restorefor every other cause. A test passes the cause to stand in for a link click or Back; see Testing routes → Simulating a link click or Back/Forward. :bypass-leave? trueon the opts map skips the current route's:can-leaveguard once.- A URL that no route matches, whose values fail the route's schemas, or with malformed percent-encoding resolves to the not-found route. Guards still decide whether that route may commit.
- A URL identical to the current location does nothing. Otherwise the guards run as for
:rf.route/navigate. The address bar has already moved, so a blocked or denied change puts the current route's URL back by replacing it. - A change to the fragment alone updates
:fragmentwithout a new navigation token or a re-run of:on-match.
- The opts map's
- Example:
[:rf.route/navigation-blocked pending]¶
- Kind: event, dispatched by the runtime
-
Payload:
pending, the value the runtime stores in the pending-navigation slot:Key Value :idThe pending-navigation id that :rf.route/continueand:rf.route/canceltake.:destinationWhere the user was going, as a request :rf.route/navigateaccepts:{:to :params :query :fragment}for a registered route,{:url …}for a URL no route matches.:targetThe resolved target, {:route-id :params :query :fragment :url}.:cause:link,:navigate,:popstate,:initialor:ssr.:policyThe request's :replace?and:scroll, or{}.:requested-urlThe URL that was requested. :rejecting-routeThe current route's id. :rejecting-guardThe id of the :can-leavesubscription that returnedfalse.:url-restored?Present and truewhen the runtime put the address bar back, after a URL-driven change. -
Description: Dispatched once when the current route's
:can-leaveguard returnsfalse. The route stays where it is, and the pending value is readable from:rf/pending-navigationuntil:rf.route/continueor:rf.route/cancelclears it. The slot holds one value, and a later block replaces it. The default handler does nothing; register your own to react, for example by opening a confirm dialog. The payload's:requested-url,:destinationand:targetare redacted in traces, with or without your handler. See Routing → Blocking a navigation and the recipe Guard against unsaved changes. - Example:
[:rf.route/entry-denied denial]¶
- Kind: event, dispatched by the runtime
- Payload:
denial,{:destination :target :cause :requested-url :guard}. The first four are as in:rf.route/navigation-blocked;:guardis the id of the target's:can-entersubscription. - Description: Dispatched once when the target route's
:can-enterguard returnsfalse. The denial is final: nothing commits, nothing is stored and there is nothing to continue.- After a URL-driven change the runtime puts the current route's URL back.
- On a server frame the runtime sets the response status to 403 before dispatching, and your handler can replace it with
:rf.server/redirector:rf.server/set-status. - The default handler does nothing. Register your own to redirect, for example to a sign-in page. After sign-in, dispatch a fresh
[:rf.route/navigate destination], and the guard runs again. See Routing → Guarding entry.
- Example:
[:rf.route/continue pending-nav-id]¶
- Kind: event
- Payload:
pending-nav-id, the pending value's:id. - Description: Goes ahead with a blocked navigation ("yes, leave the page"). Clears the pending slot and replays its
:destinationand:policythrough:rf.route/navigatewith a one-shot:bypass-leave? true, so the target's:can-enterstill runs. When the address bar was put back (:url-restored?), the replay replaces the history entry instead of pushing one. An id that does not match the pending value does nothing.
[:rf.route/cancel pending-nav-id]¶
- Kind: event
- Payload:
pending-nav-id, the pending value's:id. - Description: Abandons a blocked navigation ("stay here") and clears the pending slot. The route and the URL stay as they are. An id that does not match the pending value does nothing.
[:rf.route/prefetch {address}]¶
- Kind: event
- Payload: a named address,
{:to :params :query}.:fragmentis accepted and plays no part, because it is never a resource input;:urlis refused. - Description: Warms a destination's resource plan without navigating. It runs the parent-to-leaf plan a navigation would, in warm mode: every ensure is ownerless and
:blocking?has no effect. No route state, guards or:on-matchrun, and the warm-up stays in the frame that dispatched it. Without the resources artefact, or with an empty plan, it only emits its:rf.route/prefetchedsummary trace.route-link's:prefetch :intentdispatches it. See Routing → Warming a destination before the click. -
Errors:
:rf.error/prefetch-bad-addressrejects before planning, dispatches no resource ensures and leaves the active route unchanged. Its:reasonnames the failure::reasonCause :request-not-a-mapThe payload is not a map. :unknown-keysA key is outside :to,:params,:query,:fragment; policy keys and:urlare not accepted.:missing-to:tois absent or is not a keyword.:bad-addressA present :paramsor:queryis not a map, or:fragmentis neither a string nornil.:no-such-routeThe route id is not registered. :missing-route-paramURL construction needs a path param that is absent, nilor empty.:route-url-validationThe address fails route-url's pattern or schema checks.:route-url-non-edn-valueA supplied value has no supported URL representation. :unresolved-destinationDestination resolution threw without a routing error id. A valid destination whose resource plan cannot be built instead emits
:rf.error/resource-route-planwith:plan-cause :prefetch. No partial ensures run, and the active route's readiness stays unchanged. A failed warm fetch is reported on the resource itself, not on:rf.route/error. - Example:
[:rf.route/replan-resources {:cause cause}]¶
- Kind: event
- Payload:
{:cause cause}.:causeis required and must not benil. - Description: Reruns the active route's resource plan against the current
app-dbwithout navigating. Use it when an identity input (principal, tenant, locale) changed with no route change: a{:from-db …}subscription re-keys on its own but stays:idleuntil something ensures the new key. See Routing → Replanning the active route's resources.- It keeps the same navigation token, owner and planner. Kept identities with reusable data or in-flight work are adopted with no fetch. Added identities, and retained ones with neither data nor live work, are ensured under the route owner with your
:cause; dropped ones lose the owner. The plan, the blocking facts and readiness are replaced, so a successful replan clears an earlier:rf.error/resource-route-plan. - A planning failure commits as a failed replan: nothing is partly ensured, and the owner is released from every earlier identity.
- Reusable data is not reloaded, and no guards,
:on-match, URL, history or scroll work runs. Without the resources artefact a valid request does nothing.
- It keeps the same navigation token, owner and planner. Kept identities with reusable data or in-flight work are adopted with no fetch. Added identities, and retained ones with neither data nor live work, are ensured under the route owner with your
-
Errors:
:rf.error/replan-bad-requestrejects before planning and leaves the active route unchanged. Its:reasonis one of::reasonCause :bad-event-arityThe event is not exactly [:rf.route/replan-resources {:cause …}].:not-a-mapThe payload is not a map. :unknown-keyThe payload has a key other than :cause.:missing-cause:causeis absent ornil.:no-active-routeNo route is active yet. A resource planning failure instead commits
:transition :errorwith:rf.error/resource-route-plan, emits the diagnostic with:plan-cause :replanand:replan-causeset to your cause, and releases the previous plan's owner. - Example:
Subscriptions¶
Read the route and the pending-navigation slot with ordinary subscribe calls. Each frame has its own route, and a subscription reads the frame it runs in, so the query vectors carry no frame argument. To read another frame, pass subscribe's {:frame <target>} opts.
Before the first navigation commits, :rf/route and each :rf.route/* projection
are nil, including :rf.route/transition and :rf.route/chain. A denied initial
entry leaves them nil. The table describes an active route.
(:route-id @(rf/subscribe [:rf/route])) ;; the active route id, or nil before the first navigation
;; Show an "unsaved changes?" prompt only while a navigation is blocked.
(when-let [pending @(rf/subscribe [:rf/pending-navigation])]
[confirm-leave-dialog pending])
| Sub | Returns |
|---|---|
:rf/route |
The route slice {:route-id :params :query :fragment :transition :error :nav-token}. The :rf.route/* subscriptions below are projections of it. |
:rf.route/id |
The current route id (the slice's :route-id). |
:rf.route/params |
The current path params. |
:rf.route/query |
The current query params. |
:rf.route/transition |
:idle, :loading or :error, derived from the blocking :resources in the route's 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 when the resources artefact is not loaded. A background refresh, a non-blocking read, an intent prefetch and :on-match never move it. |
:rf.route/error |
When :transition is :error, the structured failure: :rf.error/resource-route-blocking for a blocking first-load failure, :rf.error/resource-route-plan for a plan that could not be built. Otherwise nil. |
:rf.route/fragment |
The current URL fragment, a string or nil. |
:rf.route/chain |
A vector of route ids from the outermost parent to the current route, following :parent. |
:rf/pending-navigation |
The pending-navigation slot (the :rf.route/navigation-blocked payload) while a :can-leave guard holds a navigation, otherwise nil. A denied entry is final and never appears here. |
Effects (fx)¶
| Fx | Args | Platforms | Notes |
|---|---|---|---|
[:rf.nav/push-url url-string] |
URL string | :client |
Pushes a new URL onto the browser history. |
[:rf.nav/replace-url url-string] |
URL string | :client |
Replaces the current URL without adding a history entry. |
[:rf.nav/scroll scroll-spec] |
{:strategy :from :to :saved-pos :fragment} |
:client |
Restores or sets the scroll position after the new route renders. :strategy is :top, :restore or :preserve; any other value emits :rf.error/unsupported-scroll-strategy. Navigation emits this effect for you from the route's :scroll. |
[:rf.nav/capture-scroll {:url url-string}] |
{:url ...} map |
:client |
Saves the current scroll position in the frame's scroll cache under url, before leaving a route. |
[:rf.route/with-nav-token {:rf/reply-to <reply-target> :nav-token <token>}] |
{:rf/reply-to :nav-token :route-id? :value? :completed-at?} |
:client, :server |
Completes an async continuation, named by its :rf/reply-to reply target, only if its navigation token is still current. On a match, the target is completed with the :status :ok reply map. If a later navigation has superseded the token, the completion is suppressed and :rf.route.nav-token/stale-suppressed fires. :value is carried in the :status :ok reply map. The reply's :rf.reply/work-id is [:rf.work/route route-id nav-token loader-id], where route-id is the optional :route-id (nil without it) and loader-id is the :rf/reply-to event id. :completed-at, a completion time from the :rf/time-ms cofx, is copied onto the reply and the stale trace. |
:rf.route/with-nav-token handles a user navigating away mid-load: the older load's reply carries a stale token, so the runtime suppresses it and the older page's data does not overwrite the newer page's state. See Routing → Hand-rolled async loader.
Coeffects (cofx)¶
Declare these on a handler with :rf.cofx/requires. Each value is delivered under its cofx key in the coeffects map. Both work on client and server.
| Cofx | Delivers |
|---|---|
:rf.route/nav-token |
The current navigation token, from [:rf.runtime/routing :current :nav-token]. Declare {:rf.cofx/requires [:rf.route/nav-token]} on a handler reached from :on-match to capture the token when the work is scheduled and pass it to an async continuation; :rf.route/with-nav-token checks it on receipt. |
:rf.route/route-id |
The current route id, from [:rf.runtime/routing :current :route-id]. Declare it beside :rf.route/nav-token ({:rf.cofx/requires [:rf.route/nav-token :rf.route/route-id]}) and pass it to :rf.route/with-nav-token as :route-id, so the reply and the stale trace name the route the load started on. |
URL and route matching¶
match-url reads a URL into route data, and route-url renders route data back into a URL; match-url of a route-url result gives back the canonical route data. Both are pure and run on the JVM.
match-url¶
- Kind: function
- Signature:
-
Description: Matches a URL against the registered routes and returns the route data.
- Returns
nilwhen no route matches, and when any part of the URL has malformed percent-encoding. - Path params and declared query keys come back coerced by the route's schemas. When the coerced values fail those schemas,
:validation-failed?istrueand the explanation is under:validation-error; this check runs only when the schemas artefact is loaded. - Query keys the route declares (in
:queryor:query-defaults) come back as keywords, in a deterministic canonical order. Undeclared keys stay strings. - Missing query keys receive their
:query-defaultsvalues. A present empty value stays"", so it does not use the default.
Declared slot type URL string becomes :intA whole, exactly representable integer; a string such as "12abc"stays a string.:uuidA UUID when parsing succeeds; otherwise the original string. :booleantruefor"true",falsefor"false"; other strings remain strings.[:enum :new :top]The declared keyword for its token ( "new"becomes:new); an unknown token stays a string.[:maybe type]The same conversion as the wrapped type. Other supported schema types The original string. :doubleand bare:keywordare rejected at registration.Coercion runs without the schemas artefact; validation requires it. Invalid strings therefore remain values unless
re-frame.schemasis loaded.Query parsing percent-decodes keys and values.
%20is a space and+is a literal plus, as in path params. Duplicate keys keep the last value;?flagand?flag=both mean{"flag" ""}. Empty pairs from?,&&or a trailing&are ignored, and only the first=in a pair separates key from value. - Example: - Returns
The cell below registers four routes and shows what match-url returns for each
sample URL; add your own to samples and press Mod-Enter. /articles/new beats
/articles/:id on literal segments, the optional slug group matches with or
without its segment, :id is coerced to an integer, an undeclared query key stays
a string, and a non-integer id is flagged :validation-failed?.
(require '[re-frame.core :as rf]
'[re-frame.routing :as rf.routing])
(rf/reg-route :app/home {} "/")
(rf/reg-route :app/article-new {} "/articles/new")
(rf/reg-route :app/article
{:params [:map [:id :int] [:slug {:optional true} :string]]}
"/articles/:id{/:slug}?")
(rf/reg-route :app/file {} "/files/*rest")
(def samples
["/articles/7/intro?tab=comments" "/articles/new" "/articles/7"
"/articles/seven" "/files/a/b.txt" "/no/such/path"])
(rf/reg-event :url-demo/pick
(fn [{:keys [db]} [_ url]]
{:db (assoc db :url-demo/url url)}))
(rf/reg-sub :url-demo/url (fn [db _] (:url-demo/url db (first samples))))
(rf/reg-view url-matcher []
(let [url @(subscribe [:url-demo/url])]
[:div
(for [sample samples]
[:button {:key sample
:style {:margin "0 0.5em 0.5em 0"}
:on-click #(dispatch [:url-demo/pick sample])}
sample])
[:p [:code (pr-str (list 'rf.routing/match-url url))]]
[:pre {:style {:white-space "pre-wrap"}}
(pr-str (rf.routing/match-url url))]]))
[url-matcher]
route-url¶
- Kind: function
- Signature:
- Description: Builds the URL for a route, the inverse of
match-url, from one address map.:tois the only required key. Requests name the route with:to; results such asmatch-urland the route slice name it:route-id.:fragmentappends#fragmentwhen it is a non-empty string;niland""append nothing.- Query keys with
nilvalues are left out. Anilrequired path param is an error. - Optional groups are emitted only when every param inside them is non-
nil; otherwise the whole group is omitted. Declare those params optional in the schema too. A literal-only optional group is always emitted. Sequential optional groups must be filled from left to right. false,0and""are retained as query values; an empty path segment is rejected. Values are percent-encoded, with/preserved as a separator inside a named splat. A declared keyword enum emits its token (:newasnew).- A query key already at the route's
:query-defaultsvalue is left out, becausematch-urlfills it back and spelling it would give one destination two URLs. Validation still runs against the full query you passed. - Query keys are percent-encoded, in a deterministic canonical order.
- It takes an address only, and there is no in-place form, because a pure function cannot read the current route.
- Errors:
:rf.error/no-such-route: the:toroute is not registered.:rf.error/missing-route-param: a required path segment's param isnil, absent or"". An empty segment would be dropped when the URL is matched, so it cannot round-trip.:rf.error/route-url-validation, for any of these:- the address is not a map (
:reason :not-a-map), or has no:to(:reason :missing-to); - the map carries a non-address key such as
:url,:query-merge,:replace?,:scroll,:bypass-leave?or an unknown key (:reason :bad-address-keys); :paramscarries a key the route's pattern does not capture (:reason :uncaptured-params);:paramsor:queryfail the route's schemas;:paramsfills a later optional group while an earlier one is left out. Sequential optional groups are filled in order, ormatch-urlwould read the value into the earlier group.
- the address is not a map (
:rf.error/route-url-non-edn-value: a param or query value has no portable EDN form (a function, atom or other host object, a fractional number, or an integer too large for both hosts to hold exactly), or is an instant orDate; or the fragment is neither a string nornil.
- Example:
;; with (rf/reg-route :user/show {} "/users/:id") registered: (rf.routing/route-url {:to :user/show :params {:id 42}}) ;; => "/users/42" ;; with (rf/reg-route :search {} "/search") registered, ;; 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:
- Description: Returns
truewhen any percent-encoded part ofurlis malformed: a non-empty path segment, a query key or value, or the#fragment. The check is lexical and consults no routes.:rf.route/handle-url-changeuses it to tell a plain route miss ({:url url}) from a malformed URL that failed closed ({:url url :reason :malformed-url}). Both end at:rf.route/not-found; the:reasonlets error pages and SSR branch on the cause.
Querying registered routes¶
There is no route-ids or route-meta function. Use the generic registrar queries, registrations and handler-meta:
(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 holds the :path pattern and everything the registration declared (see reg-route's options), plus the computed :rf.route/rank, :rf.route/compiled and coercion tables and the source coordinates. Unlike a resource, mutation or resource-scope registration, it keeps this metadata at the top level rather than under an inner key.
URL strategies¶
A URL strategy decides how the app URL, the path-form URL that routes match (/active?q=milk), appears in the browser's address bar. Declare one on the URL-owning frame with the :url-strategy config key, as in the examples below. A frame without one uses history-url-strategy.
Choosing one:
- Keep the default
history-url-strategywhen your server answers every app path with the app's page, so reloading/articles/introworks. The URLs are the plain paths. - Use
hash-url-strategywhen it cannot, as on a static host without rewrite rules: the route lives after the#, so the server only ever serves the page itself. - Wrap either with
with-base-pathwhen the app is not served from the site root, for example under/realworld/.
A strategy is a map of five functions, {:encode :decode :push! :replace! :install-listener!}; A custom strategy gives each one's contract. It is consulted at four points: the two history effects, the route-link href, and decoding an incoming URL (the URL listener, and a {:url …} or :rf.route/url-requested URL that carries an origin). route-url, match-url and navigation itself always work in path form.
:push!, :replace! and :install-listener! exist only in ClojureScript. SSR runs none of them, because the server reads the request URL through :rf.route/handle-url-change and has no history. It does apply :encode, so a server-rendered route-link has the same href as the hydrated client: /demos/active for a with-base-path frame, #/active for a hash frame.
history-url-strategy¶
- Kind: var
- Signature:
- Description: The default strategy: HTML5 History with path-form URLs.
:encodeand:decodeare identity.:push!and:replace!callpushStateandreplaceState.:install-listener!listens forpopstate.
hash-url-strategy¶
- Kind: var
- Signature:
- Description: Puts the route in the URL fragment (
#/active), for static hosting without server rewrites and for apps coming from hash-based routing such as secretary.route-urlstill builds the path form/active.:encodeturns it into#/activefor theroute-linkhref and the history effects.:decodetakes the origin-relative browser address and returns what follows its first#; a missing or empty fragment decodes to/.:install-listener!listens forhashchange.
- Example:
with-base-path¶
- Kind: function
- Signature:
- Description: Wraps
strategyso the app can be deployed under a sub-path, such as/realworld/on a host that mounts several demos side by side. It works with either shipped strategy or your own; it is not a third strategy.:encodeaddsbaseto every outbound href, outside whatever formstrategyproduces:/realworld/activefor history,/realworld#/activefor hash.:decodeand:install-listener!stripbasefrom every inbound URL. A fragment-form strategy's:decodenever sees the base, so its result passes through unchanged.:push!and:replace!are not wrapped, because the href they receive is already encoded, base included.route-url,match-urland navigation stay path-form and know nothing of the base.- A blank or
nilbasereturnsstrategyunchanged.
- Example:
A custom strategy¶
Any map carrying these five functions is a strategy, and extra keys are kept.
| Key | Signature | Contract |
|---|---|---|
:encode |
(fn [path] href) |
Turns an app URL (/active?q=milk) into the href the address bar and route-link show. Pure; runs on both hosts. |
:decode |
(fn [href] path) |
The inverse: takes the origin-relative browser address (pathname + search + hash) and returns the app URL. Pure, and reads no window. For every app URL p, (decode (encode p)) is p. |
:push! |
(fn [href]) |
Adds a history entry for href, which :encode has already produced. It must not encode again. ClojureScript only. |
:replace! |
(fn [href]) |
Replaces the current history entry, taking href as :push! does. ClojureScript only. |
:install-listener! |
(fn [on-change] teardown) |
Installs the browser's URL-change listener and returns a zero-argument teardown function. Calls on-change with the decoded app URL on each browser-driven change. The runtime syncs the current URL itself when it installs the listener, so this function does not. ClojureScript only. |
make-frame checks a declared :url-strategy. In ClojureScript every one of the five keys must hold a function; on the JVM, :encode and :decode. Anything else, an explicit nil included, throws :rf.error/invalid-url-strategy, naming the keys that are missing or not functions. A new frame is then not created, and a re-registered one keeps its previous config.
with-base-path treats a strategy as fragment-form when (encode "/") returns a string starting with #, so a custom hash-style strategy takes a base path the way hash-url-strategy does.
Multi-frame URL ownership¶
At most one frame owns the browser URL at a time. A frame claims it by being created with {:url-bound? true}. Both the outbound :rf.nav/push-url effect and the inbound browser listener use url-owner-frame-id to find the owner. See Routing → Several frames, one address bar.
url-owner-frame-id¶
- Kind: function
- Signature:
- Description: Returns the frame that declared browser-history ownership with
(rf/make-frame {:id … :url-bound? true}), ornilwhen none has.- Ownership is always declared: like any other frame,
:rf/defaultowns the URL only when it is created with{:url-bound? true}. - The owner is the first still-live frame that claimed
:url-bound? true, so a later duplicate cannot take the URL. Creating the duplicate emits:rf.error/duplicate-url-binding, naming both frames; if the owner is destroyed, the next claimant takes over. - Frames that claimed
:url-bound? truebeforere-frame.routingloaded have no recorded order. One such frame becomes the owner; with two or more, the runtime emits:rf.error/duplicate-url-bindingfor each extra and no frame owns the URL until one of those frames is registered again or destroyed. - With no owner, the outbound history effects do nothing and the inbound listener skips its dispatch.
- Ownership is always declared: like any other frame,
- Example:
Browser URL listener¶
The browser popstate or hashchange listener is installed for you. When a :url-bound? true frame is created or re-registered and becomes the URL owner, the runtime installs the listener and syncs the current URL into that frame's route slice. Destroying the frame removes the listener. There is no install-url-listener!, remove-url-listener!, install-history-listener! or remove-history-listener! to call.
- Each browser-driven change is decoded to an app URL by the strategy's
:decode, then dispatched synchronously as:rf.route/handle-url-changeto(url-owner-frame-id), resolved when the change fires. With no:url-bound? trueframe, nothing is dispatched. - The listener kind (
popstateorhashchange) comes from the owning frame's:url-strategywhen the listener is installed. - Installing is idempotent. A re-registration with the same owner and
:url-strategyleaves the listener alone; a changed owner or strategy removes the old listener before installing the new one. A duplicate:url-bound? trueframe that loses to the current owner never installs one. - ClojureScript only. On the JVM there is nothing to install; SSR passes the request URL to
:rf.route/handle-url-change.
Framework integration¶
Not for application code — used by adapters, tools and the test harness.
The static route view and the live route-slice view that Xray draws have no public accessor: tools require re-frame.routing.tooling directly.
Server-side rendering¶
route-link-render-ssr¶
- Kind: function
- Signature:
- Description: The JVM render function for the
:route/linkview. It renders the<a href=...>without click handling, since the server has no DOM events; on the hydrated page, clicks go through the ClojureScript view.- The href is encoded through the rendering frame's
:url-strategy, as on the client. SSR sets the request frame for its render, so awith-base-pathserver frame renders/demos/activeand a hash frame#/active, matching the hydrated render. Called outside any frame it renders the path form, as the history strategy would. - Application code writes
route-link.
- The href is encoded through the rendering frame's
Scroll restoration¶
Saved scroll positions are kept in a per-frame LRU cache on the host, keyed by frame id, not in runtime-db. They are read from window.scrollX/Y, mean nothing on the server, and are not needed to rebuild a frame on restore, SSR hydration or time-travel. So the cache is never part of runtime-db, epochs or SSR payloads, and an epoch restore does not rewind it. A single position can still appear in a trace as the :saved-pos argument of a :rf.nav/scroll effect. The pure functions work on one frame's cache map, {:positions {url [x y]} :order [url ...]}; the ! functions read and write the host cache.
scroll-positions-cap¶
- Kind: var
- Signature:
- Description: The maximum number of URLs one frame's scroll cache holds. Saving past it evicts the least recently used URL.
frame-scroll-cache¶
- Kind: function
- Signature:
- Description: Returns the cache map
{:positions :order}forframe-idfrom the host cache, ornilwhen there is none. Navigation planning reads this value.
lookup-scroll-position¶
- Kind: function
- Signature:
- Description: Returns the saved
[x y]forurlincache, ornil. Pure.cacheis one frame's cache map{:positions {url [x y]} :order [...]}, and may itself benil.
save-scroll-position¶
- Kind: function
- Signature:
- Description: Returns
cachewith the position forurlrecorded under:positions. Pure.- The cache holds at most
scroll-positions-capURLs. Saving a URL again makes it the most recent, and a new save past the cap evicts the least recently used one. The:ordervector records recency.
- The cache holds at most
save-scroll-position!¶
- Kind: function
- Signature:
- Description: Records
xyforurlinframe-id's host cache, applying the cap throughsave-scroll-position.
Navigation counters and state classification¶
The counters that allocate navigation tokens and pending-navigation ids are per-frame high-water marks kept on the host, not in runtime-db. An epoch restore replaces runtime-db wholesale; keeping the counters outside it means a restore cannot rewind them and reissue a token that a slow in-flight continuation still holds.
counter-snapshot¶
- Kind: function
- Signature:
- Description: Returns the counters for
frame-idfrom the host cache, or{}when there are none. The allocation coeffects take the next navigation token and pending-navigation id from this value.
routing-state-classification¶
- Kind: var
- Signature:
- Description: Classifies every piece of per-frame routing state by tier, so the durable/transient split is defined in one place. SSR's payload policy takes its hydration keys from the
:durable-runtime-dbtier.:durable-runtime-db: serializable facts needed to rebuild a coherent frame on restore or SSR hydration. This is the route slice at:current.:local-subscribable-runtime-db:runtime-dbstate that stays subscribable and restores in local replay, but is stripped from SSR payloads. This is the:pending-navigationslot.:host-transient: caches on the host, never inruntime-db. These are the saved scroll positions and the two counters.
Test helpers¶
Each of these resets process-wide routing state so it does not leak from one test into the next. The fixture built by make-reset-runtime-fixture calls reset-counters!, reset-nav-counters! and reset-url-claims!.
reset-counters!¶
- Kind: function
- Signature:
- Description: Resets the route-registration counter to zero, so the registration-order tiebreak in route ranking is the same on every fixture run.
reset-scroll-cache!¶
- Kind: function
- Signature:
- Description: Clears the whole host scroll cache.
reset-nav-counters!¶
- Kind: function
- Signature:
- Description: Clears the whole host navigation-counter cache.
reset-url-claims!¶
- Kind: function
- Signature:
- Description: Clears the URL-ownership claim order, so the next test starts with no
:url-bound?claim.
See also¶
- re-frame.core: the facade entries for
reg-routeandroute-link. - re-frame.ssr: routes take part in SSR, and
head-modellooks up the active route's:head. - Routing glossary: navigate, route, loader, route guard, not-found,
url-bound?. - Coming from React Router: how the concepts map, and where re-frame2 routing differs.