URL State & Routing Rules
June 9, 2026 · View on GitHub
This app uses a hybrid routing approach: path routes for major navigation and identity, plus query params for view-state and filters. The goal is:
- Refresh keeps you on the same view
- Links are shareable for the views that matter
- URL logic stays consistent and maintainable
What goes in the path vs query params
Put it in the path when it answers “what page/resource is this?”
Use path segments for:
- Top-level navigation (major app areas)
- Example:
/upload,/editor,/errors
- Example:
- Resource identity (something you’d fetch by ID)
- Example:
/editor/company/:companyId
- Example:
- Major mode changes that fundamentally change what the screen is
- Example:
/editor/multi-company(if we choose to model this as a route)
- Example:
- States that must behave like pages in browser history
- If users expect back/forward to traverse it like page navigation, it should usually be a route.
Rule of thumb: If changing it would reasonably warrant a new breadcrumb label or page title, it’s probably a route.
Put it in query params when it answers “how is the page currently configured?”
Use query params for:
- Tabs/subtabs inside a page
- Example:
/editor/company/a1b2c3d4-e5f6-7890-abcd-ef1234567890?tab=emissions(full internal UUID; legacy/editor/company/Q123still resolves via read API)
- Example:
- Filters and search
- Example:
?q=volvo&tags=auto,sweden&years=2023,2024
- Example:
- Sorting and paging
- Example:
?sort=updatedAt:desc&page=2
- Example:
- UI preferences / toggles (open/closed panels, “show unverified”, etc.)
- Example:
?filtersOpen=1&unverifiedOnly=1
- Example:
Rule of thumb: If it’s safe to drop and the page still “makes sense”, it’s usually a query param.
Required behavior
- Every route-level screen must be reconstructible from the URL.
- If the user refreshes, the app must land on the same top-level tab and (where applicable) the same selected resource.
- Query params should be optional.
- If absent, the screen should choose sensible defaults (e.g. default tab).
- Unknown/invalid values should fail safe.
- If
?tab=not-a-real-tab, fall back to default (don’t crash).
- If
- Changing URL state must not break deep links.
- Prefer additive changes; if we must rename params, keep compatibility for at least one release.
Naming conventions (query params)
Keep names short, explicit, and stable.
- General
tab: active tab/subtab within the current routeq: search query (only if context is obvious; otherwise usesearch)sort: sort key and direction (e.g.updatedAt:desc)page,pageSize: paginationfilters: filters enabled
- Booleans
- Use
1/0(preferred) ortrue/false, but be consistent per param. - Examples:
filtersOpen=1,unverifiedOnly=0
- Use
- Lists
- Use comma-separated values for short lists:
tags=a,b,c,years=2023,2024 - Values must be URL-safe; if not, encode each item.
- Use comma-separated values for short lists:
- Do not encode large blobs
- If it’s long enough to be annoying or exceed typical URL limits, it should not be in the URL. Store it elsewhere (server, local storage) and keep a stable key in the URL.
Canonical examples we will follow
These examples are meant to be copied by humans and AI tools.
Top-level tabs (route)
/upload/editor/errors/crawler/registry/jobbstatus/workflow/debug/access— API key management (auth-gated; tab is hidden in the nav until logged in)
Jobbstatus (Live vs Archive)
The route is /jobbstatus only. Live and Archive are in-page tabs (not separate path segments). The active subtab is synced to the URL so refresh and shared links preserve it:
- Live (default):
/jobbstatus— omitsourceor use?source=live. - Archive:
/jobbstatus?source=archive.
Invalid source values fall back to Live. Switching to Live removes the source query param (cleaner default URL).
/climate-plans
Upload (query params)
- Upload mode is a view config → query param:
/upload?mode=url/upload?mode=file
Editor (route + query params)
- Editor as a top-level area → route:
/editor
- Selected company is a resource identity → route param:
/editor/company/:companyId
- Company detail subtab is a view config → query param:
/editor/company/Q123?tab=company-detail/editor/company/Q123?tab=emissions/editor/company/Q123?tab=economy/editor/company/Q123?tab=reporting-periods
Filters (query params)
Filters belong in query params when:
- They help a user return to the same list state after refresh
- They are reasonably short
Example:
/editor?q=volvo&tags=auto,sweden&years=2023,2024&unverifiedOnly=1
Decision checklist (quick)
Before adding any new URL state, answer:
- Is this identifying a resource?
- Yes → path
- Is this selecting a major area/mode that should feel like navigation?
- Yes → usually path
- Is this configuring a view (tab/filter/sort/toggle)?
- Yes → query param
- Will users expect back/forward to traverse it as a page?
- Yes → prefer path (or at minimum write history entries when changing it)
- Could it be large or sensitive?
- Yes → do not put it in URL (store it elsewhere)
Implementation guidelines (React Router)
- Prefer using
useSearchParams()for query params and keep components controlled by URL state. - Prefer
useParams()for route params like:companyId. - When updating query params, use a helper so we don’t accidentally wipe unrelated params.
- Do not rely on
defaultValuefor tabs that should persist across refresh; bindvalueto URL-derived state.